├── .dockerignore
├── .env.example
├── .eslintrc.json
├── app
├── favicon.ico
├── services
│ └── api-client.ts
├── components
│ ├── PDFViewer.css
│ ├── Spinner.tsx
│ ├── Banner.tsx
│ ├── SelectDocsAsk.tsx
│ ├── FileSearcher.tsx
│ ├── Chatbot.tsx
│ ├── ChatHistory.tsx
│ ├── Pagination.tsx
│ ├── FileUploader.tsx
│ ├── PDFViewer.tsx
│ └── QuestionField.tsx
├── globals.css
├── page.tsx
└── layout.tsx
├── images
├── Preview.png
└── architecture.png
├── postcss.config.js
├── public
├── assets
│ └── RAG-banner.png
├── vercel.svg
└── next.svg
├── Dockerfile
├── .gitignore
├── tailwind.config.ts
├── tsconfig.json
├── prisma
├── schema.prisma
└── client.ts
├── README.md
├── next.config.js
└── package.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 |
--------------------------------------------------------------------------------
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nelsonlin0321/webdev-nextjs-rag/HEAD/app/favicon.ico
--------------------------------------------------------------------------------
/images/Preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nelsonlin0321/webdev-nextjs-rag/HEAD/images/Preview.png
--------------------------------------------------------------------------------
/images/architecture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nelsonlin0321/webdev-nextjs-rag/HEAD/images/architecture.png
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/assets/RAG-banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nelsonlin0321/webdev-nextjs-rag/HEAD/public/assets/RAG-banner.png
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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"]
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | {/* */}
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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | ```
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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 |
66 |
67 |
68 | );
69 | };
70 |
71 | export default FileUploader;
72 |
--------------------------------------------------------------------------------
/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/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 |
107 | );
108 | };
109 |
110 | export default QuestionField;
111 |
--------------------------------------------------------------------------------