├── .dockerignore ├── .env.example ├── .eslintrc.json ├── .gitignore ├── Dockerfile ├── README.md ├── app ├── components │ ├── Banner.tsx │ ├── ChatHistory.tsx │ ├── Chatbot.tsx │ ├── FileSearcher.tsx │ ├── FileUploader.tsx │ ├── PDFViewer.css │ ├── PDFViewer.tsx │ ├── Pagination.tsx │ ├── QuestionField.tsx │ ├── SelectDocsAsk.tsx │ └── Spinner.tsx ├── favicon.ico ├── globals.css ├── layout.tsx ├── page.tsx └── services │ └── api-client.ts ├── images ├── Preview.png └── architecture.png ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── prisma ├── client.ts └── schema.prisma ├── public ├── assets │ └── RAG-banner.png ├── next.svg └── vercel.svg ├── tailwind.config.ts └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | export NEXT_PUBLIC_BACKEND_URL= 2 | export DATABASE_URL= -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 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 | .env 38 | docker.env 39 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-buster 2 | RUN mkdir /app 3 | COPY package.json /app/ 4 | WORKDIR /app 5 | COPY . ./ 6 | 7 | ENV NEXT_PUBLIC_BACKEND_URL=${NEXT_PUBLIC_BACKEND_URL} 8 | ENV DATABASE_URL=${DATABASE_URL} 9 | 10 | RUN npm install 11 | RUN npx prisma generate && npx prisma db push 12 | RUN npm run build 13 | EXPOSE 3000 14 | CMD ["npm", "run","start"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Full Stack Implementation to Build an RAG (Retrieval Augmented Generation) Application 2 | 3 | ## Brief Introduction 4 | A Next.js Retrieval-Augmented Generation (RAG) Application that allow users to upload PDF document and ask questions related to the selected document. 5 | It achieves this by retrieving pertinent data or documents related to a specific question or task and utilizing them as contextual information for the LLM. 6 | 7 | ## Preview 8 | 9 | 10 | 11 | ## Architecture 12 | 13 | 14 | 15 | ## Tech Stack 16 | - Framework: Next.js with React 17 | - Database: MongoDB 18 | - ORM: Prisma 19 | - UI: Material UI, Semantic UI, Radix UI 20 | 21 | ## Run Locally 22 | ```shell 23 | npm install 24 | npm run dev 25 | ``` 26 | 27 | ## Backend 28 | Github: https://github.com/Nelsonlin0321/webdev-rag-backend-api 29 | 30 | ## Build Docker 31 | ```shell 32 | image_name=rag-nextjs-app 33 | docker build -t ${image_name}:latest -f ./Dockerfile . --platform linux/arm64/v8 34 | docker run --env-file docker.env -p 3000:3000 -it --rm ${image_name}:latest --name ${image_name} 35 | ``` -------------------------------------------------------------------------------- /app/components/Banner.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | // import Image from "next/image"; 3 | import { Heading, Text } from "@radix-ui/themes"; 4 | 5 | const Banner = () => { 6 | return ( 7 | <> 8 | {/* Picture of the author */} 14 | 18 | Empower Your Organizations Document Intelligence with RAG 19 | 20 | 21 | Upload your PDF and Talk to them with AI! 22 | 23 | 24 | ); 25 | }; 26 | 27 | export default Banner; 28 | -------------------------------------------------------------------------------- /app/components/ChatHistory.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Text } from "@radix-ui/themes"; 3 | import dynamic from "next/dynamic"; 4 | import { useEffect, useState } from "react"; 5 | import { Accordion, Icon, Label } from "semantic-ui-react"; 6 | import { chatRecord } from "./Chatbot"; 7 | 8 | const PDFViewer = dynamic(() => import("./PDFViewer"), { 9 | ssr: false, 10 | }); 11 | 12 | interface Props { 13 | chatRecords: chatRecord[]; 14 | } 15 | 16 | const ChatHistory = ({ chatRecords }: Props) => { 17 | const [activeIndex, setActiveIndex] = useState(0); 18 | 19 | useEffect(() => { 20 | setActiveIndex(0); 21 | }, [chatRecords]); 22 | 23 | return ( 24 | 25 | {chatRecords.map((message, index) => ( 26 |
27 | { 30 | if (activeIndex == index) { 31 | setActiveIndex(-1); 32 | } else { 33 | setActiveIndex(index); 34 | } 35 | }} 36 | > 37 | 38 | {message.question} 39 |
40 | 45 |
46 |
47 | 48 | 49 | 50 | {message.answer} 51 | 52 | 56 | 57 |
58 | ))} 59 |
60 | ); 61 | }; 62 | 63 | export default ChatHistory; 64 | -------------------------------------------------------------------------------- /app/components/Chatbot.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { useEffect, useState } from "react"; 3 | import SelectDocsAsk from "./SelectDocsAsk"; 4 | import ChatHistory from "./ChatHistory"; 5 | import { Button } from "semantic-ui-react"; 6 | 7 | interface Props { 8 | fileNames: string[]; 9 | } 10 | 11 | export interface chatRecord { 12 | question: string; 13 | file_name: string; 14 | answer: string; 15 | uuid: string; 16 | page_number: number; 17 | } 18 | 19 | export const Chatbot = ({ fileNames }: Props) => { 20 | const initChatRecords = [ 21 | { 22 | context: "total population in Hong Kong Island", 23 | question: 24 | "What is the percentage of total population in Hong Kong Island ? ", 25 | file_name: "Hong Kong Fact Sheets - Population.pdf", 26 | answer: 27 | "Answer: 17.5% of the total population in Hong Kong is in Hong Kong Island. Page Number->1.", 28 | page_number: 1, 29 | uuid: "77fa6513-8224-4bbd-a7aa-3ea89ed5d4cd", 30 | }, 31 | ]; 32 | 33 | const [chatRecords, setChatRecords] = useState(initChatRecords); 34 | 35 | useEffect(() => { 36 | const savedChatRecords = localStorage.getItem("chatRecords"); 37 | if (savedChatRecords) { 38 | const parsedChatRecords = JSON.parse(savedChatRecords); 39 | if (parsedChatRecords.length > 0) { 40 | setChatRecords(parsedChatRecords); 41 | } 42 | } 43 | }, []); 44 | 45 | useEffect(() => { 46 | localStorage.setItem("chatRecords", JSON.stringify(chatRecords)); 47 | }, [chatRecords]); 48 | 49 | return ( 50 | <> 51 | 56 | 57 | 58 | 59 | ); 60 | }; 61 | -------------------------------------------------------------------------------- /app/components/FileSearcher.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import TextField from "@mui/material/TextField"; 3 | import Autocomplete from "@mui/material/Autocomplete"; 4 | import { InputAdornment } from "@mui/material"; 5 | import SearchIcon from "@mui/icons-material/Search"; 6 | import { useState } from "react"; 7 | 8 | interface Props { 9 | fileNames: string[]; 10 | setFileName: (fileName: string) => void; 11 | } 12 | 13 | const FileSearcher = ({ fileNames, setFileName }: Props) => { 14 | const [value, setValue] = useState(fileNames[0]); 15 | const [inputValue, setInputValue] = useState( 16 | fileNames[0] 17 | ); 18 | 19 | return ( 20 | { 23 | setValue(newValue); 24 | }} 25 | inputValue={inputValue} 26 | onInputChange={(event: any, newInputValue) => { 27 | setInputValue(newInputValue); 28 | setFileName(newInputValue); 29 | }} 30 | className="w-full rounded-lg border-2 bg-slate-50" 31 | disablePortal 32 | id="combo-box-demo" 33 | options={fileNames} 34 | renderInput={(params) => ( 35 | 42 | 43 | Search PDF 44 | 45 | ), 46 | }} 47 | id={params.id} 48 | inputProps={params.inputProps} 49 | fullWidth={params.fullWidth} 50 | /> 51 | )} 52 | /> 53 | ); 54 | }; 55 | 56 | export default FileSearcher; 57 | -------------------------------------------------------------------------------- /app/components/FileUploader.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { useState } from "react"; 3 | import apiClient from "../services/api-client"; 4 | import { AxiosError } from "axios"; 5 | import Spinner from "./Spinner"; 6 | import toast, { Toaster } from "react-hot-toast"; 7 | import { useRouter } from "next/navigation"; 8 | import { Button } from "semantic-ui-react"; 9 | 10 | const FileUploader = () => { 11 | const [file, setFile] = useState(); 12 | const [isSubmitting, setSubmitting] = useState(false); 13 | const router = useRouter(); 14 | 15 | const onSubmit = async (e: React.FormEvent) => { 16 | e.preventDefault(); 17 | if (!file) return; 18 | if (!file.name.endsWith(".pdf")) { 19 | toast.error("Only PDF document supported", { duration: 1000 }); 20 | return; 21 | } 22 | setSubmitting(true); 23 | try { 24 | const data = new FormData(); 25 | data.set("file", file); 26 | await apiClient.post("/api/ingest", data, { 27 | headers: { 28 | "Content-Type": "multipart/form-data", 29 | }, 30 | }); 31 | router.refresh(); 32 | toast.success("File uploaded successfully!", { duration: 1000 }); 33 | } catch (error) { 34 | const response = (error as AxiosError).response?.data; 35 | const message = (response as { message: string }).message; 36 | const errorMessage = message || "File Uploading Failed!"; 37 | toast.error(errorMessage, { duration: 1000 }); 38 | } finally { 39 | setSubmitting(false); 40 | } 41 | }; 42 | 43 | return ( 44 |
45 |
46 | { 51 | if (e.target.files !== null) { 52 | setFile(e.target.files[0]); 53 | } 54 | }} 55 | /> 56 | 65 |
66 | 67 |
68 | ); 69 | }; 70 | 71 | export default FileUploader; 72 | -------------------------------------------------------------------------------- /app/components/PDFViewer.css: -------------------------------------------------------------------------------- 1 | .react-pdf__Page__canvas { 2 | margin: 0 auto; 3 | } 4 | 5 | .PDFPage { 6 | box-shadow: 0 0 8px rgba(0, 0, 0, 0.2); 7 | margin-bottom: 25px; 8 | margin-top: 25px; 9 | } 10 | 11 | .PDFPage > canvas { 12 | max-width: 100%; 13 | height: auto !important; 14 | } 15 | -------------------------------------------------------------------------------- /app/components/PDFViewer.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { useState } from "react"; 3 | import { Document, Page } from "react-pdf"; 4 | import { pdfjs } from "react-pdf"; 5 | import "react-pdf/dist/Page/AnnotationLayer.css"; 6 | import "react-pdf/dist/Page/TextLayer.css"; 7 | import "./PDFViewer.css"; 8 | import Spinner from "./Spinner"; 9 | import { 10 | DoubleArrowLeftIcon, 11 | ChevronLeftIcon, 12 | ChevronRightIcon, 13 | DoubleArrowRightIcon, 14 | } from "@radix-ui/react-icons"; 15 | import { Flex, Text, Button } from "@radix-ui/themes"; 16 | 17 | pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.js`; 18 | 19 | interface Props { 20 | pdfUrl: string; 21 | pageNumber: number; 22 | } 23 | 24 | const PDFViewer = ({ pdfUrl, pageNumber }: Props) => { 25 | const [numPages, setNumPages] = useState(null); 26 | console.log(pageNumber); 27 | const [currentPage, changePage] = useState(pageNumber); 28 | 29 | return ( 30 | <> 31 | { 34 | setNumPages(pdf.numPages); 35 | changePage(pageNumber); 36 | }} 37 | onLoadError={(error) => console.log(error)} 38 | loading={} 39 | error={This PDF is not available} 40 | > 41 | 46 | 47 | {numPages && ( 48 | 49 | 50 | page {currentPage} of {numPages} 51 | 52 | 60 | 68 | 76 | 84 | 85 | )} 86 | 87 | ); 88 | }; 89 | 90 | export default PDFViewer; 91 | -------------------------------------------------------------------------------- /app/components/Pagination.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { 3 | ChevronLeftIcon, 4 | ChevronRightIcon, 5 | DoubleArrowLeftIcon, 6 | DoubleArrowRightIcon, 7 | } from "@radix-ui/react-icons"; 8 | import { Button, Flex, Text } from "@radix-ui/themes"; 9 | import { useRouter, useSearchParams } from "next/navigation"; 10 | import React from "react"; 11 | 12 | interface Props { 13 | itemCount: number; 14 | pageSize: number; 15 | currentPage: number; 16 | } 17 | 18 | const Pagination = ({ itemCount, pageSize, currentPage }: Props) => { 19 | const pageCount = Math.ceil(itemCount / pageSize); 20 | const searchParams = useSearchParams(); 21 | const router = useRouter(); 22 | 23 | const changePage = (page: number) => { 24 | const params = new URLSearchParams(searchParams); 25 | params.set("page", page.toString()); 26 | router.push("?" + params.toString()); 27 | }; 28 | 29 | if (pageCount <= 1) return null; 30 | return ( 31 | 32 | 33 | page {currentPage} of {pageCount} 34 | 35 | 43 | 51 | 59 | 67 | 68 | ); 69 | }; 70 | 71 | export default Pagination; 72 | -------------------------------------------------------------------------------- /app/components/QuestionField.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { TextField, Text, Heading } from "@radix-ui/themes"; 3 | import { useRef, useState } from "react"; 4 | import { Button } from "semantic-ui-react"; 5 | import toast, { Toaster } from "react-hot-toast"; 6 | import apiClient from "../services/api-client"; 7 | import { chatRecord } from "./Chatbot"; 8 | // import { AxiosError } from "axios"; 9 | import Spinner from "./Spinner"; 10 | 11 | interface Props { 12 | fileName: string; 13 | fileNames: string[]; 14 | chatRecords: chatRecord[]; 15 | setChatRecords: (records: chatRecord[]) => void; 16 | } 17 | 18 | const QuestionField = ({ 19 | fileName, 20 | fileNames, 21 | chatRecords, 22 | setChatRecords, 23 | }: Props) => { 24 | const questionRef = useRef(null); 25 | const contextRef = useRef(null); 26 | const [isLoading, setLoading] = useState(false); 27 | const submitData: { 28 | question: string; 29 | file_name: string; 30 | context?: null | string; 31 | } = { question: "", file_name: fileName }; 32 | 33 | return ( 34 |
35 |
{ 37 | event.preventDefault(); 38 | 39 | if (questionRef.current) { 40 | if (questionRef.current.value.split(" ").length < 3) { 41 | toast.error("The question requires at least 3 words"); 42 | return; 43 | } 44 | 45 | if (!fileNames.includes(fileName)) { 46 | toast.error("The selected PDF document doesn't exist!"); 47 | return; 48 | } 49 | submitData.question = questionRef.current.value; 50 | if (contextRef.current) { 51 | submitData.context = contextRef.current.value; 52 | } 53 | 54 | setLoading(true); 55 | try { 56 | await apiClient 57 | .post("/api/retrieval_generate", submitData) 58 | .then((res) => { 59 | setChatRecords([res.data, ...chatRecords]); 60 | setLoading(false); 61 | }); 62 | } catch (error) { 63 | // const response = (error as AxiosError).response?.data; 64 | // const message = (response as { message: string }).message; 65 | // const errorMessage = message || "Unexpected Error"; 66 | const errorMessage = "Unexpected Error"; 67 | toast.error(errorMessage, { duration: 1000 }); 68 | } finally { 69 | setLoading(false); 70 | } 71 | } 72 | }} 73 | > 74 |
75 | 76 | Context for better searching the relevant content [Optional] 77 | 78 | 79 | 83 | 84 | 85 | Your question related to selected PDF Document 86 | 87 | 88 | 92 | 93 | 94 | 103 |
104 | 105 | 106 |
107 | ); 108 | }; 109 | 110 | export default QuestionField; 111 | -------------------------------------------------------------------------------- /app/components/SelectDocsAsk.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { useState } from "react"; 3 | import FileSearcher from "./FileSearcher"; 4 | import QuestionField from "./QuestionField"; 5 | import { chatRecord } from "./Chatbot"; 6 | 7 | interface Props { 8 | fileNames: string[]; 9 | chatRecords: chatRecord[]; 10 | setChatRecords: (records: chatRecord[]) => void; 11 | } 12 | 13 | const SelectDocsAsk = ({ fileNames, chatRecords, setChatRecords }: Props) => { 14 | const [fileName, setFileName] = useState(fileNames[0]); 15 | 16 | return ( 17 | <> 18 | 19 | 25 | 26 | ); 27 | }; 28 | 29 | export default SelectDocsAsk; 30 | -------------------------------------------------------------------------------- /app/components/Spinner.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Spinner = () => { 4 | return ( 5 |
9 | 10 |
11 | ); 12 | }; 13 | 14 | export default Spinner; 15 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nelsonlin0321/webdev-nextjs-rag/9ad32f8cddc51133e9118b62ce06815df57ad623/app/favicon.ico -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --foreground-rgb: 0, 0, 0; 7 | --background-start-rgb: 214, 219, 220; 8 | --background-end-rgb: 255, 255, 255; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --foreground-rgb: 255, 255, 255; 14 | --background-start-rgb: 0, 0, 0; 15 | --background-end-rgb: 0, 0, 0; 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "@radix-ui/themes/styles.css"; 2 | import type { Metadata } from "next"; 3 | import { Inter } from "next/font/google"; 4 | import "./globals.css"; 5 | import "semantic-ui-css/semantic.min.css"; 6 | import { Container, Theme } from "@radix-ui/themes"; 7 | import Head from "next/head"; 8 | const inter = Inter({ subsets: ["latin"] }); 9 | 10 | export const metadata: Metadata = { 11 | title: "RAG App", 12 | description: "Empower Your Organizations Document Intelligence with RAG", 13 | }; 14 | 15 | export default function RootLayout({ 16 | children, 17 | }: { 18 | children: React.ReactNode; 19 | }) { 20 | return ( 21 | 22 | 23 | 27 | 28 | 29 | 30 |
31 | 32 | {children} 33 | 34 |
35 |
36 | 37 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import prisma from "@/prisma/client"; 2 | import { Flex } from "@radix-ui/themes"; 3 | import Banner from "./components/Banner"; 4 | import FileUploader from "./components/FileUploader"; 5 | import { Chatbot } from "./components/Chatbot"; 6 | 7 | export default async function Home() { 8 | const documents = await prisma.document.findMany(); 9 | const fileNames = documents.map(({ id, fileName }) => fileName); 10 | 11 | return ( 12 |
13 | 18 | 19 | 20 | 21 | 22 |
23 | ); 24 | } 25 | 26 | export const dynamic = "force-dynamic"; 27 | -------------------------------------------------------------------------------- /app/services/api-client.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export default axios.create({ 4 | baseURL: process.env.NEXT_PUBLIC_BACKEND_URL, 5 | }); 6 | -------------------------------------------------------------------------------- /images/Preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nelsonlin0321/webdev-nextjs-rag/9ad32f8cddc51133e9118b62ce06815df57ad623/images/Preview.png -------------------------------------------------------------------------------- /images/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nelsonlin0321/webdev-nextjs-rag/9ad32f8cddc51133e9118b62ce06815df57ad623/images/architecture.png -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | async headers() { 4 | return [{ 5 | source: "/:path*", 6 | headers: [ 7 | // Allow for specific domains to have access or * for all 8 | { 9 | key: "Access-Control-Allow-Origin", 10 | value: "*", 11 | // DOES NOT WORK 12 | // value: process.env.ALLOWED_ORIGIN, 13 | }, 14 | // Allows for specific methods accepted 15 | { 16 | key: "Access-Control-Allow-Methods", 17 | value: "GET, POST, PUT, DELETE, OPTIONS", 18 | }, 19 | // Allows for specific headers accepted (These are a few standard ones) 20 | { 21 | key: "Access-Control-Allow-Headers", 22 | value: "Content-Type, Authorization", 23 | }, 24 | ], 25 | }] 26 | }, 27 | 28 | webpack: (config) => { 29 | config.resolve.alias.canvas = false; return config 30 | }, 31 | 32 | } 33 | 34 | module.exports = nextConfig 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webdev-nextjs-rag", 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 | "@emotion/react": "^11.11.3", 13 | "@emotion/styled": "^11.11.0", 14 | "@mui/icons-material": "^5.15.1", 15 | "@mui/material": "^5.15.1", 16 | "@prisma/client": "^5.7.1", 17 | "@radix-ui/react-icons": "^1.3.0", 18 | "@radix-ui/themes": "^2.0.3", 19 | "@react-pdf/renderer": "^3.3.1", 20 | "@types/react-pdf": "^7.0.0", 21 | "axios": "^1.6.2", 22 | "delay": "^6.0.0", 23 | "next": "14.0.4", 24 | "pdfjs": "^2.5.3", 25 | "pdfjs-dist": "^4.0.379", 26 | "prisma": "^5.7.1", 27 | "react": "^18", 28 | "react-dom": "^18", 29 | "react-hot-toast": "^2.4.1", 30 | "semantic-ui-css": "^2.5.0", 31 | "semantic-ui-react": "^2.1.5", 32 | "sharp": "^0.33.1" 33 | }, 34 | "devDependencies": { 35 | "@types/node": "20.11.19", 36 | "@types/react": "18.2.57", 37 | "@types/react-dom": "^18", 38 | "autoprefixer": "^10.0.1", 39 | "eslint": "^8", 40 | "eslint-config-next": "14.0.4", 41 | "postcss": "^8", 42 | "tailwindcss": "^3.3.0", 43 | "typescript": "5.3.3" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /prisma/client.ts: -------------------------------------------------------------------------------- 1 | // as the best practice, we should make sure that there 2 | // is only a single instance of this running our application. 3 | // So the reason we are doing this in this file is because the first time 4 | // this client file is imported somewhere in out application, we get a new instance of this Prisma 5 | // but the second time this file is imported, this code is not re-executed, it's cached, so the result will be reused. 6 | // Not, in Next.js because we have fast refresh, anytime we change our source codes,Next.js 7 | // refreshes some of our modules. In that case, we will end up in a situation where we have too many Prisma clients. 8 | // This only happens in development mode. 9 | 10 | // https://www.prisma.io/docs/guides/other/troubleshooting-orm/help-articles/nextjs-prisma-client-dev-practices 11 | import { PrismaClient } from "@prisma/client"; 12 | 13 | const prismaClientSingleton = () => { 14 | return new PrismaClient({ log: ["info"] }); 15 | }; 16 | 17 | type PrismaClientSingleton = ReturnType; 18 | 19 | const globalForPrisma = globalThis as unknown as { 20 | prisma: PrismaClientSingleton | undefined; 21 | }; 22 | 23 | const prisma = globalForPrisma.prisma ?? prismaClientSingleton(); 24 | 25 | export default prisma; 26 | 27 | if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma; 28 | -------------------------------------------------------------------------------- /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 Document { 14 | id String @id @default(auto()) @map("_id") @db.ObjectId 15 | fileName String @unique 16 | DocumentEmbedding DocumentEmbedding[] 17 | } 18 | 19 | model DocumentEmbedding { 20 | id String @id @default(auto()) @map("_id") @db.ObjectId 21 | fileName String 22 | document Document @relation(fields: [fileName], references: [fileName]) 23 | textIdx Int 24 | pageLabel String 25 | text String 26 | embedding Json 27 | } 28 | -------------------------------------------------------------------------------- /public/assets/RAG-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nelsonlin0321/webdev-nextjs-rag/9ad32f8cddc51133e9118b62ce06815df57ad623/public/assets/RAG-banner.png -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss' 2 | 3 | const config: Config = { 4 | content: [ 5 | './pages/**/*.{js,ts,jsx,tsx,mdx}', 6 | './components/**/*.{js,ts,jsx,tsx,mdx}', 7 | './app/**/*.{js,ts,jsx,tsx,mdx}', 8 | ], 9 | theme: { 10 | extend: { 11 | backgroundImage: { 12 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 13 | 'gradient-conic': 14 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', 15 | }, 16 | }, 17 | }, 18 | plugins: [], 19 | } 20 | export default config 21 | -------------------------------------------------------------------------------- /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 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | --------------------------------------------------------------------------------