├── .eslintrc.json ├── .gitignore ├── README.md ├── components.json ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── prettier.config.js ├── prisma └── schema.prisma ├── public ├── next.svg └── vercel.svg ├── 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 ├── 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 /.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 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Next.js 14 AI Note App 2 | 3 | This is a note-taking app with an integrated **AI chatbot**. By using the **ChatGPT API**, **vector embeddings**, and **Pinecone**, the chatbot knows about all notes stored in your user account and can retrieve relevant information to answer your questions and summarize information. 4 | 5 | **Response streaming** is implemented via the **Vercel AI SDK**. 6 | 7 | The app is built with Next.js 14's app router, TailwindCSS, Shadcn UI, and TypeScript. It has a light/dark theme toggle and a fully mobile-responsive layout. 8 | 9 | Learn how to build this app in my tutorial: https://www.youtube.com/watch?v=mkJbEP5GeRA 10 | 11 | ![thumbnail](https://github.com/codinginflow/nextjs-ai-note-app/assets/52977034/cefc69f2-a486-4072-bf69-d0738f7336af) 12 | -------------------------------------------------------------------------------- /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 | }, 11 | "dependencies": { 12 | "@clerk/nextjs": "^4.26.1", 13 | "@clerk/themes": "^1.7.9", 14 | "@hookform/resolvers": "^3.3.2", 15 | "@pinecone-database/pinecone": "^1.1.2", 16 | "@prisma/client": "^5.5.2", 17 | "@radix-ui/react-dialog": "^1.0.5", 18 | "@radix-ui/react-label": "^2.0.2", 19 | "@radix-ui/react-slot": "^1.0.2", 20 | "ai": "^2.2.20", 21 | "class-variance-authority": "^0.7.0", 22 | "clsx": "^2.0.0", 23 | "eslint-config-prettier": "^9.0.0", 24 | "lucide-react": "^0.290.0", 25 | "next": "14.0.0", 26 | "next-themes": "^0.2.1", 27 | "openai": "^4.14.1", 28 | "prettier": "^3.0.3", 29 | "prettier-plugin-tailwindcss": "^0.5.6", 30 | "prisma": "^5.5.2", 31 | "react": "^18", 32 | "react-dom": "^18", 33 | "react-hook-form": "^7.47.0", 34 | "tailwind-merge": "^1.14.0", 35 | "tailwindcss-animate": "^1.0.7", 36 | "zod": "^3.22.4" 37 | }, 38 | "devDependencies": { 39 | "@types/node": "^20", 40 | "@types/react": "^18", 41 | "@types/react-dom": "^18", 42 | "autoprefixer": "^10", 43 | "eslint": "^8", 44 | "eslint-config-next": "14.0.0", 45 | "postcss": "^8", 46 | "tailwindcss": "^3", 47 | "typescript": "^5" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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/codinginflow/nextjs-ai-note-app/929bab3ab79c2b723eacc273a637c064acbd651a/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: "FlowBrain", 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 | FlowBrain logo 27 | FlowBrain 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: "FlowBrain - 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 | FlowBrain logo 17 | 18 | FlowBrain 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: "FlowBrain - 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: "FlowBrain - 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/codinginflow/nextjs-ai-note-app/929bab3ab79c2b723eacc273a637c064acbd651a/src/assets/logo.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 |
83 | 93 | 99 | 100 |
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 | User image 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 | 99 | 100 | 101 | {noteToEdit ? "Edit Note" : "Add Note"} 102 | 103 |
104 | 105 | ( 109 | 110 | Note title 111 | 112 | 113 | 114 | 115 | 116 | )} 117 | /> 118 | ( 122 | 123 | Note content 124 | 125 |