├── .env.example
├── .gitignore
├── .prettierrc
├── LICENSE
├── README.md
├── app
├── api
│ ├── chat
│ │ └── route.ts
│ ├── ingestPdf
│ │ └── route.ts
│ └── utils
│ │ ├── embeddings
│ │ └── index.ts
│ │ └── vector_store
│ │ ├── index.ts
│ │ ├── mongo.ts
│ │ └── pinecone.ts
├── dashboard
│ ├── dashboard-client.tsx
│ ├── layout.tsx
│ └── page.tsx
├── document
│ └── [id]
│ │ ├── document-client.tsx
│ │ ├── layout.tsx
│ │ └── page.tsx
├── layout.tsx
├── page.tsx
├── sign-in
│ └── [[...sign-in]]
│ │ └── page.tsx
└── sign-up
│ └── [[...sign-up]]
│ └── page.tsx
├── components
├── home
│ ├── Footer.tsx
│ ├── Header.tsx
│ ├── Hero.tsx
│ ├── HowItWorks.tsx
│ └── ProudlyOpenSource.tsx
└── ui
│ ├── DocIcon.tsx
│ ├── Header.tsx
│ ├── LoadingDots.tsx
│ ├── Logo.tsx
│ ├── TextArea.tsx
│ └── Toggle.tsx
├── middleware.ts
├── next.config.js
├── package-lock.json
├── package.json
├── postcss.config.js
├── prisma
└── schema.prisma
├── public
├── align-justify.svg
├── bot-icon.png
├── chat.png
├── custom-chat-bg.png
├── favicon.ico
├── github.png
├── logo.png
├── og-image.png
├── pen.png
├── profile-icon.png
├── right-arrow.svg
├── upload.png
├── user.svg
└── usericon.png
├── styles
├── globals.css
└── loading-dots.module.css
├── tailwind.config.js
├── tsconfig.json
└── utils
├── chatType.ts
├── cn.ts
├── config.ts
├── prisma.ts
└── ragChain.ts
/.env.example:
--------------------------------------------------------------------------------
1 | TOGETHER_AI_API_KEY=
2 | NEXT_PUBLIC_BYTESCALE_API_KEY=
3 |
4 | # The vector store you'd like to use.
5 | # Can be one of "pinecone" or "mongodb"
6 | # Defaults to "pinecone"
7 | NEXT_PUBLIC_VECTORSTORE="pinecone"
8 |
9 | # Update these with your pinecone details from your dashboard.
10 | # Not required if `NEXT_PUBLIC_VECTORSTORE` is set to "mongodb"
11 | PINECONE_API_KEY=
12 | PINECONE_INDEX_NAME= # PINECONE_INDEX_NAME is in the indexes tab under "index name" in blue
13 |
14 | # Update these with your MongoDB Atlas details from your dashboard.
15 | # Not required if `NEXT_PUBLIC_VECTORSTORE` is set to "pinecone"
16 | MONGODB_ATLAS_URI=
17 | MONGODB_ATLAS_DB_NAME=
18 | MONGODB_ATLAS_COLLECTION_NAME=
19 | MONGODB_ATLAS_INDEX_NAME=
20 |
21 | CLERK_SECRET_KEY=
22 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=
23 | NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
24 | NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
25 |
26 | POSTGRES_URL=
27 | POSTGRES_URL_NON_POOLING=
28 | POSTGRES_PRISMA_URL=
29 |
30 | # Enable tracing via smith.langchain.com
31 | # LANGCHAIN_TRACING_V2=true
32 | # LANGCHAIN_API_KEY=
33 | # LANGCHAIN_SESSION=pdftochat
34 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | .pnpm-debug.log*
27 |
28 | # local env files
29 | .env*.local
30 | .env
31 |
32 | # vercel
33 | .vercel
34 |
35 | # typescript
36 | *.tsbuildinfo
37 | next-env.d.ts
38 |
39 | #Notion_db
40 | /Notion_DB
41 |
42 | .yarn/
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "all",
3 | "singleQuote": true,
4 | "printWidth": 80,
5 | "tabWidth": 2
6 | }
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License Copyright (c) 2023 Hassan El Mghari
2 |
3 | Permission is hereby granted, free of
4 | charge, to any person obtaining a copy of this software and associated
5 | documentation files (the "Software"), to deal in the Software without
6 | restriction, including without limitation the rights to use, copy, modify, merge,
7 | publish, distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to the
9 | following conditions:
10 |
11 | The above copyright notice and this permission notice
12 | (including the next paragraph) shall be included in all copies or substantial
13 | portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
18 | EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
19 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | PDFToChat
4 |
5 |
6 |
7 | Chat with your PDFs in seconds. Powered by Together AI and Pinecone.
8 |
9 |
10 |
11 | Tech Stack ·
12 | Deploy Your Own ·
13 | Common Errors
14 | ·
15 | Credits
16 | ·
17 | Future Tasks
18 |
19 |
20 |
21 | ## Tech Stack
22 |
23 | - Next.js [App Router](https://nextjs.org/docs/app) for the framework
24 | - Mixtral through [Together AI](https://togetherai.link) inference for the LLM
25 | - M2 Bert 80M through [Together AI](https://togetherai.link) for embeddings
26 | - [LangChain.js](https://js.langchain.com/docs/get_started/introduction/) for the RAG code
27 | - [MongoDB Atlas](https://www.mongodb.com/atlas/database) for the vector database
28 | - [Bytescale](https://www.bytescale.com/) for the PDF storage
29 | - [Vercel](https://vercel.com/) for hosting and for the postgres DB
30 | - [Clerk](https://clerk.dev/) for user authentication
31 | - [Tailwind CSS](https://tailwindcss.com/) for styling
32 |
33 | ## Deploy Your Own
34 |
35 | You can deploy this template to Vercel or any other host. Note that you'll need to:
36 |
37 | - Set up [Together.ai](https://togetherai.link)
38 | - Set up a [MongoDB Atlas](https://www.mongodb.com/atlas/database) Atlas database with 768 dimensions
39 | - See instructions below for MongoDB
40 | - Set up [Bytescale](https://www.bytescale.com/)
41 | - Set up [Clerk](https://clerk.dev/)
42 | - Set up [Vercel](https://vercel.com/)
43 | - (Optional) Set up [LangSmith](https://smith.langchain.com/) for tracing.
44 |
45 | See the .example.env for a list of all the required environment variables.
46 |
47 | You will also need to prepare your database schema by running `npx prisma db push`.
48 |
49 | ### MongoDB Atlas
50 |
51 | To set up a [MongoDB Atlas](https://www.mongodb.com/atlas/database) database as the backing vectorstore, you will need to perform the following steps:
52 |
53 | 1. Sign up on their website, then create a database cluster. Find it under the `Database` sidebar tab.
54 | 2. Create a **collection** by switching to `Collections` the tab and creating a blank collection.
55 | 3. Create an **index** by switching to the `Atlas Search` tab and clicking `Create Search Index`.
56 | 4. Make sure you select `Atlas Vector Search - JSON Editor`, select the appropriate database and collection, and paste the following into the textbox:
57 |
58 | ```json
59 | {
60 | "fields": [
61 | {
62 | "numDimensions": 768,
63 | "path": "embedding",
64 | "similarity": "euclidean",
65 | "type": "vector"
66 | },
67 | {
68 | "path": "docstore_document_id",
69 | "type": "filter"
70 | }
71 | ]
72 | }
73 | ```
74 |
75 | Note that the `numDimensions` is 768 dimensions to match the embeddings model we're using, and that we have another index on `docstore_document_id`. This allows us to filter later.
76 |
77 | You may call the index whatever you wish, just make a note of it!
78 |
79 | 5. Finally, retrieve and set the following environment variables:
80 |
81 | ```ini
82 | NEXT_PUBLIC_VECTORSTORE=mongodb # Set MongoDB Atlas as your vectorstore
83 |
84 | MONGODB_ATLAS_URI= # Connection string for your database.
85 | MONGODB_ATLAS_DB_NAME= # The name of your database.
86 | MONGODB_ATLAS_COLLECTION_NAME= # The name of your collection.
87 | MONGODB_ATLAS_INDEX_NAME= # The name of the index you just created.
88 | ```
89 |
90 | ## Common errors
91 |
92 | - Check that you've created an `.env` file that contains your valid (and working) API keys, environment and index name.
93 | - Check that you've set the vector dimensions to `768` and that `index` matched your specified field in the `.env variable`.
94 | - Check that you've added a credit card on Together AI if you're hitting rate limiting issues due to the free tier
95 |
96 | ## Credits
97 |
98 | - [Youssef](https://twitter.com/YoussefUiUx) for the design of the app
99 | - [Mayo](https://twitter.com/mayowaoshin) for the original RAG repo and inspiration
100 | - [Jacob](https://twitter.com/Hacubu) for the LangChain help
101 | - Together AI, Bytescale, Pinecone, and Clerk for sponsoring
102 |
103 | ## Future tasks
104 |
105 | These are some future tasks that I have planned. Contributions are welcome!
106 |
107 | - [ ] Add a trash icon for folks to delete PDFs from the dashboard and implement delete functionality
108 | - [ ] Try different embedding models like UAE-large-v1 to see if it improves accuracy
109 | - [ ] Explore best practices for auto scrolling based on other chat apps like chatGPT
110 | - [ ] Do some prompt engineering for Mixtral to make replies as good as possible
111 | - [ ] Protect API routes by making sure users are signed in before executing chats
112 | - [ ] Run an initial benchmark on how accurate chunking / retrieval are
113 | - [ ] Research best practices for chunking and retrieval and play around with them – ideally run benchmarks
114 | - [ ] Try out Langsmith for more observability into how the RAG app runs
115 | - [ ] Add demo video to the homepage to demonstrate functionality more easily
116 | - [ ] Upgrade to Next.js 14 and fix any issues with that
117 | - [ ] Implement sources like perplexity to be clickable with more info
118 | - [ ] Add analytics to track the number of chats & errors
119 | - [ ] Make some changes to the default tailwind `prose` to decrease padding
120 | - [ ] Add an initial message with sample questions or just add them as bubbles on the page
121 | - [ ] Add an option to get answers as markdown or in regular paragraphs
122 | - [ ] Implement something like SWR to automatically revalidate data
123 | - [ ] Save chats for each user to get back to later in the postgres DB
124 | - [ ] Bring up a message to direct folks to compress PDFs if they're beyond 10MB
125 | - [ ] Use a self-designed custom uploader
126 | - [ ] Use a session tracking tool to better understand how folks are using the site
127 | - [ ] Add better error handling overall with appropriate toasts when actions fail
128 | - [ ] Add support for images in PDFs with something like [Nougat](https://replicate.com/meta/nougat)
129 |
--------------------------------------------------------------------------------
/app/api/chat/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from 'next/server';
2 | import type { Message as VercelChatMessage } from 'ai';
3 | import { createRAGChain } from '@/utils/ragChain';
4 |
5 | import type { Document } from '@langchain/core/documents';
6 | import { HumanMessage, AIMessage, ChatMessage } from '@langchain/core/messages';
7 | import { ChatTogetherAI } from '@langchain/community/chat_models/togetherai';
8 | import { type MongoClient } from 'mongodb';
9 | import { loadRetriever } from '../utils/vector_store';
10 | import { loadEmbeddingsModel } from '../utils/embeddings';
11 |
12 | export const runtime =
13 | process.env.NEXT_PUBLIC_VECTORSTORE === 'mongodb' ? 'nodejs' : 'edge';
14 |
15 | const formatVercelMessages = (message: VercelChatMessage) => {
16 | if (message.role === 'user') {
17 | return new HumanMessage(message.content);
18 | } else if (message.role === 'assistant') {
19 | return new AIMessage(message.content);
20 | } else {
21 | console.warn(
22 | `Unknown message type passed: "${message.role}". Falling back to generic message type.`,
23 | );
24 | return new ChatMessage({ content: message.content, role: message.role });
25 | }
26 | };
27 |
28 | /**
29 | * This handler initializes and calls a retrieval chain. It composes the chain using
30 | * LangChain Expression Language. See the docs for more information:
31 | *
32 | * https://js.langchain.com/docs/get_started/quickstart
33 | * https://js.langchain.com/docs/guides/expression_language/cookbook#conversational-retrieval-chain
34 | */
35 | export async function POST(req: NextRequest) {
36 | let mongoDbClient: MongoClient | undefined;
37 |
38 | try {
39 | const body = await req.json();
40 | const messages = body.messages ?? [];
41 | if (!messages.length) {
42 | throw new Error('No messages provided.');
43 | }
44 | const formattedPreviousMessages = messages
45 | .slice(0, -1)
46 | .map(formatVercelMessages);
47 | const currentMessageContent = messages[messages.length - 1].content;
48 | const chatId = body.chatId;
49 |
50 | const model = new ChatTogetherAI({
51 | modelName: 'mistralai/Mixtral-8x7B-Instruct-v0.1',
52 | temperature: 0,
53 | });
54 |
55 | const embeddings = loadEmbeddingsModel();
56 |
57 | let resolveWithDocuments: (value: Document[]) => void;
58 | const documentPromise = new Promise((resolve) => {
59 | resolveWithDocuments = resolve;
60 | });
61 |
62 | const retrieverInfo = await loadRetriever({
63 | chatId,
64 | embeddings,
65 | callbacks: [
66 | {
67 | handleRetrieverEnd(documents) {
68 | // Extract retrieved source documents so that they can be displayed as sources
69 | // on the frontend.
70 | resolveWithDocuments(documents);
71 | },
72 | },
73 | ],
74 | });
75 |
76 | const retriever = retrieverInfo.retriever;
77 | mongoDbClient = retrieverInfo.mongoDbClient;
78 |
79 | const ragChain = await createRAGChain(model, retriever);
80 |
81 | const stream = await ragChain.stream({
82 | input: currentMessageContent,
83 | chat_history: formattedPreviousMessages,
84 | });
85 |
86 | const documents = await documentPromise;
87 | const serializedSources = Buffer.from(
88 | JSON.stringify(
89 | documents.map((doc) => {
90 | return {
91 | pageContent: doc.pageContent.slice(0, 50) + '...',
92 | metadata: doc.metadata,
93 | };
94 | }),
95 | ),
96 | ).toString('base64');
97 |
98 | // Convert to bytes so that we can pass into the HTTP response
99 | const byteStream = stream.pipeThrough(new TextEncoderStream());
100 |
101 | return new Response(byteStream, {
102 | headers: {
103 | 'x-message-index': (formattedPreviousMessages.length + 1).toString(),
104 | 'x-sources': serializedSources,
105 | },
106 | });
107 | } catch (e: any) {
108 | return NextResponse.json({ error: e.message }, { status: 500 });
109 | } finally {
110 | if (mongoDbClient) {
111 | await mongoDbClient.close();
112 | }
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/app/api/ingestPdf/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server';
2 | import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter';
3 | import { PDFLoader } from 'langchain/document_loaders/fs/pdf';
4 | import prisma from '@/utils/prisma';
5 | import { getAuth } from '@clerk/nextjs/server';
6 | import { loadEmbeddingsModel } from '../utils/embeddings';
7 | import { loadVectorStore } from '../utils/vector_store';
8 | import { type MongoClient } from 'mongodb';
9 |
10 | export async function POST(request: Request) {
11 | let mongoDbClient: MongoClient | null = null;
12 |
13 | const { fileUrl, fileName, vectorStoreId } = await request.json();
14 |
15 | const { userId } = getAuth(request as any);
16 |
17 | if (!userId) {
18 | return NextResponse.json({ error: 'You must be logged in to ingest data' });
19 | }
20 |
21 | const docAmount = await prisma.document.count({
22 | where: {
23 | userId,
24 | },
25 | });
26 |
27 | if (docAmount > 3) {
28 | return NextResponse.json({
29 | error: 'You have reached the maximum number of documents',
30 | });
31 | }
32 |
33 | const doc = await prisma.document.create({
34 | data: {
35 | fileName,
36 | fileUrl,
37 | userId,
38 | },
39 | });
40 |
41 | const namespace = doc.id;
42 |
43 | try {
44 | /* load from remote pdf URL */
45 | const response = await fetch(fileUrl);
46 | const buffer = await response.blob();
47 | const loader = new PDFLoader(buffer);
48 | const rawDocs = await loader.load();
49 |
50 | /* Split text into chunks */
51 | const textSplitter = new RecursiveCharacterTextSplitter({
52 | chunkSize: 1000,
53 | chunkOverlap: 200,
54 | });
55 | const splitDocs = await textSplitter.splitDocuments(rawDocs);
56 | // Necessary for Mongo - we'll query on this later.
57 | for (const splitDoc of splitDocs) {
58 | splitDoc.metadata.docstore_document_id = namespace;
59 | }
60 |
61 | console.log('creating vector store...');
62 |
63 | /* create and store the embeddings in the vectorStore */
64 | const embeddings = loadEmbeddingsModel();
65 |
66 | const store = await loadVectorStore({
67 | namespace: doc.id,
68 | embeddings,
69 | });
70 | const vectorstore = store.vectorstore;
71 | if ('mongoDbClient' in store) {
72 | mongoDbClient = store.mongoDbClient;
73 | }
74 |
75 | // embed the PDF documents
76 | await vectorstore.addDocuments(splitDocs);
77 | } catch (error) {
78 | console.log('error', error);
79 | return NextResponse.json({ error: 'Failed to ingest your data' });
80 | } finally {
81 | if (mongoDbClient) {
82 | await mongoDbClient.close();
83 | }
84 | }
85 |
86 | return NextResponse.json({
87 | text: 'Successfully embedded pdf',
88 | id: namespace,
89 | });
90 | }
91 |
--------------------------------------------------------------------------------
/app/api/utils/embeddings/index.ts:
--------------------------------------------------------------------------------
1 | import { TogetherAIEmbeddings } from '@langchain/community/embeddings/togetherai';
2 |
3 | export function loadEmbeddingsModel() {
4 | return new TogetherAIEmbeddings({
5 | apiKey: process.env.TOGETHER_AI_API_KEY,
6 | modelName: 'togethercomputer/m2-bert-80M-8k-retrieval',
7 | });
8 | }
9 |
--------------------------------------------------------------------------------
/app/api/utils/vector_store/index.ts:
--------------------------------------------------------------------------------
1 | import { Embeddings } from '@langchain/core/embeddings';
2 | import { loadPineconeStore } from './pinecone';
3 | import { loadMongoDBStore } from './mongo';
4 | import { Callbacks } from '@langchain/core/callbacks/manager';
5 |
6 | export async function loadVectorStore({
7 | namespace,
8 | embeddings,
9 | }: {
10 | namespace: string;
11 | embeddings: Embeddings;
12 | }) {
13 | const vectorStoreEnv = process.env.NEXT_PUBLIC_VECTORSTORE ?? 'pinecone';
14 |
15 | if (vectorStoreEnv === 'pinecone') {
16 | return await loadPineconeStore({
17 | namespace,
18 | embeddings,
19 | });
20 | } else if (vectorStoreEnv === 'mongodb') {
21 | return await loadMongoDBStore({
22 | embeddings,
23 | });
24 | } else {
25 | throw new Error(`Invalid vector store id provided: ${vectorStoreEnv}`);
26 | }
27 | }
28 |
29 | export async function loadRetriever({
30 | embeddings,
31 | chatId,
32 | callbacks,
33 | }: {
34 | // namespace: string;
35 | embeddings: Embeddings;
36 | chatId: string;
37 | callbacks?: Callbacks;
38 | }) {
39 | let mongoDbClient;
40 | const store = await loadVectorStore({
41 | namespace: chatId,
42 | embeddings,
43 | });
44 | const vectorstore = store.vectorstore;
45 | if ('mongoDbClient' in store) {
46 | mongoDbClient = store.mongoDbClient;
47 | }
48 | // For Mongo, we will use metadata filtering to separate documents.
49 | // For Pinecone, we will use namespaces, so no filter is necessary.
50 | const filter =
51 | process.env.NEXT_PUBLIC_VECTORSTORE === 'mongodb'
52 | ? {
53 | preFilter: {
54 | docstore_document_id: {
55 | $eq: chatId,
56 | },
57 | },
58 | }
59 | : undefined;
60 | const retriever = vectorstore.asRetriever({
61 | filter,
62 | callbacks,
63 | });
64 | return {
65 | retriever,
66 | mongoDbClient,
67 | };
68 | }
69 |
--------------------------------------------------------------------------------
/app/api/utils/vector_store/mongo.ts:
--------------------------------------------------------------------------------
1 | import { MongoClient } from 'mongodb';
2 | import { MongoDBAtlasVectorSearch } from '@langchain/mongodb';
3 | import { Embeddings } from '@langchain/core/embeddings';
4 |
5 | export async function loadMongoDBStore({
6 | embeddings,
7 | }: {
8 | embeddings: Embeddings;
9 | }) {
10 | const mongoDbClient = new MongoClient(process.env.MONGODB_ATLAS_URI ?? '');
11 |
12 | await mongoDbClient.connect();
13 |
14 | const dbName = process.env.MONGODB_ATLAS_DB_NAME ?? '';
15 | const collectionName = process.env.MONGODB_ATLAS_COLLECTION_NAME ?? '';
16 | const collection = mongoDbClient.db(dbName).collection(collectionName);
17 |
18 | const vectorstore = new MongoDBAtlasVectorSearch(embeddings, {
19 | indexName: process.env.MONGODB_ATLAS_INDEX_NAME ?? 'vector_index',
20 | collection,
21 | });
22 |
23 | return {
24 | vectorstore,
25 | mongoDbClient,
26 | };
27 | }
28 |
--------------------------------------------------------------------------------
/app/api/utils/vector_store/pinecone.ts:
--------------------------------------------------------------------------------
1 | import { Embeddings } from '@langchain/core/embeddings';
2 | import { Pinecone } from '@pinecone-database/pinecone';
3 | import { PineconeStore } from '@langchain/pinecone';
4 |
5 | export async function loadPineconeStore({
6 | namespace,
7 | embeddings,
8 | }: {
9 | namespace: string;
10 | embeddings: Embeddings;
11 | }) {
12 | const pinecone = new Pinecone({
13 | apiKey: process.env.PINECONE_API_KEY ?? '',
14 | });
15 |
16 | const PINECONE_INDEX_NAME = process.env.PINECONE_INDEX_NAME ?? '';
17 | const index = pinecone.index(PINECONE_INDEX_NAME);
18 |
19 | const vectorstore = await PineconeStore.fromExistingIndex(embeddings, {
20 | pineconeIndex: index,
21 | namespace,
22 | textKey: 'text',
23 | });
24 |
25 | return {
26 | vectorstore,
27 | };
28 | }
29 |
--------------------------------------------------------------------------------
/app/dashboard/dashboard-client.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { UploadDropzone } from 'react-uploader';
4 | import { Uploader } from 'uploader';
5 | import { useRouter } from 'next/navigation';
6 | import DocIcon from '@/components/ui/DocIcon';
7 | import { formatDistanceToNow } from 'date-fns';
8 | import { useState } from 'react';
9 |
10 | // Configuration for the uploader
11 | const uploader = Uploader({
12 | apiKey: !!process.env.NEXT_PUBLIC_BYTESCALE_API_KEY
13 | ? process.env.NEXT_PUBLIC_BYTESCALE_API_KEY
14 | : 'no api key found',
15 | });
16 |
17 | export default function DashboardClient({ docsList }: { docsList: any }) {
18 | const router = useRouter();
19 |
20 | const [loading, setLoading] = useState(false);
21 |
22 | const options = {
23 | maxFileCount: 1,
24 | mimeTypes: ['application/pdf'],
25 | editor: { images: { crop: false } },
26 | styles: {
27 | colors: {
28 | primary: '#000', // Primary buttons & links
29 | error: '#d23f4d', // Error messages
30 | },
31 | },
32 | onValidate: async (file: File): Promise => {
33 | return docsList.length > 3
34 | ? `You've reached your limit for PDFs.`
35 | : undefined;
36 | },
37 | };
38 |
39 | const UploadDropZone = () => (
40 | {
44 | if (file.length !== 0) {
45 | setLoading(true);
46 | ingestPdf(
47 | file[0].fileUrl,
48 | file[0].originalFile.originalFileName || file[0].filePath,
49 | );
50 | }
51 | }}
52 | width="470px"
53 | height="250px"
54 | />
55 | );
56 |
57 | async function ingestPdf(fileUrl: string, fileName: string) {
58 | let res = await fetch('/api/ingestPdf', {
59 | method: 'POST',
60 | headers: {
61 | 'Content-Type': 'application/json',
62 | },
63 | body: JSON.stringify({
64 | fileUrl,
65 | fileName,
66 | }),
67 | });
68 |
69 | let data = await res.json();
70 | router.push(`/document/${data.id}`);
71 | }
72 |
73 | return (
74 |
75 |
76 | Chat With Your PDFs
77 |
78 | {docsList.length > 0 && (
79 |
80 |
81 | {docsList.map((doc: any) => (
82 |
86 |
93 | {formatDistanceToNow(doc.createdAt)} ago
94 |
95 | ))}
96 |
97 |
98 | )}
99 | {docsList.length > 0 ? (
100 |
101 | Or upload a new PDF
102 |
103 | ) : (
104 |
105 | No PDFs found. Upload a new PDF below!
106 |
107 | )}
108 |
109 | {loading ? (
110 |
136 | ) : (
137 |
138 | )}
139 |
140 |
141 | );
142 | }
143 |
--------------------------------------------------------------------------------
/app/dashboard/layout.tsx:
--------------------------------------------------------------------------------
1 | import Footer from '@/components/home/Footer';
2 | import Header from '@/components/ui/Header';
3 |
4 | export default function RootLayout({
5 | children,
6 | }: {
7 | children: React.ReactNode;
8 | }) {
9 | return (
10 |
11 |
12 |
{children}
13 |
14 |
15 |
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/app/dashboard/page.tsx:
--------------------------------------------------------------------------------
1 | import DashboardClient from './dashboard-client';
2 | import prisma from '@/utils/prisma';
3 | import { currentUser } from '@clerk/nextjs';
4 | import type { User } from '@clerk/nextjs/api';
5 |
6 | export default async function Page() {
7 | const user: User | null = await currentUser();
8 |
9 | const docsList = await prisma.document.findMany({
10 | where: {
11 | userId: user?.id,
12 | },
13 | orderBy: {
14 | createdAt: 'desc',
15 | },
16 | });
17 |
18 | return (
19 |
20 |
21 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/app/document/[id]/document-client.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useRef, useState, useEffect } from 'react';
4 | import Image from 'next/image';
5 | import ReactMarkdown from 'react-markdown';
6 | import LoadingDots from '@/components/ui/LoadingDots';
7 | import { Viewer, Worker } from '@react-pdf-viewer/core';
8 | import '@react-pdf-viewer/core/lib/styles/index.css';
9 | import '@react-pdf-viewer/default-layout/lib/styles/index.css';
10 | import type {
11 | ToolbarSlot,
12 | TransformToolbarSlot,
13 | } from '@react-pdf-viewer/toolbar';
14 | import { toolbarPlugin } from '@react-pdf-viewer/toolbar';
15 | import { pageNavigationPlugin } from '@react-pdf-viewer/page-navigation';
16 | import { Document } from '@prisma/client';
17 | import { useChat } from 'ai/react';
18 | import Toggle from '@/components/ui/Toggle';
19 |
20 | export default function DocumentClient({
21 | currentDoc,
22 | userImage,
23 | }: {
24 | currentDoc: Document;
25 | userImage?: string;
26 | }) {
27 | const toolbarPluginInstance = toolbarPlugin();
28 | const pageNavigationPluginInstance = pageNavigationPlugin();
29 | const { renderDefaultToolbar, Toolbar } = toolbarPluginInstance;
30 |
31 | const transform: TransformToolbarSlot = (slot: ToolbarSlot) => ({
32 | ...slot,
33 | Download: () => <>>,
34 | SwitchTheme: () => <>>,
35 | Open: () => <>>,
36 | });
37 |
38 | const chatId = currentDoc.id;
39 | const pdfUrl = currentDoc.fileUrl;
40 |
41 | const [sourcesForMessages, setSourcesForMessages] = useState<
42 | Record
43 | >({});
44 | const [error, setError] = useState('');
45 | const [chatOnlyView, setChatOnlyView] = useState(false);
46 |
47 | const { messages, input, handleInputChange, handleSubmit, isLoading } =
48 | useChat({
49 | api: '/api/chat',
50 | body: {
51 | chatId,
52 | },
53 | onResponse(response) {
54 | const sourcesHeader = response.headers.get('x-sources');
55 | const sources = sourcesHeader ? JSON.parse(atob(sourcesHeader)) : [];
56 |
57 | const messageIndexHeader = response.headers.get('x-message-index');
58 | if (sources.length && messageIndexHeader !== null) {
59 | setSourcesForMessages({
60 | ...sourcesForMessages,
61 | [messageIndexHeader]: sources,
62 | });
63 | }
64 | },
65 | onError: (e) => {
66 | setError(e.message);
67 | },
68 | onFinish() {},
69 | });
70 |
71 | const messageListRef = useRef(null);
72 | const textAreaRef = useRef(null);
73 |
74 | useEffect(() => {
75 | textAreaRef.current?.focus();
76 | }, []);
77 |
78 | // Prevent empty chat submissions
79 | const handleEnter = (e: any) => {
80 | if (e.key === 'Enter' && messages) {
81 | handleSubmit(e);
82 | } else if (e.key == 'Enter') {
83 | e.preventDefault();
84 | }
85 | };
86 |
87 | let userProfilePic = userImage ? userImage : '/profile-icon.png';
88 |
89 | const extractSourcePageNumber = (source: {
90 | metadata: Record;
91 | }) => {
92 | return source.metadata['loc.pageNumber'] ?? source.metadata.loc?.pageNumber;
93 | };
94 | return (
95 |
96 |
97 |
98 | {/* Left hand side */}
99 |
100 |
105 |
111 | {renderDefaultToolbar(transform)}
112 |
113 |
117 |
118 |
119 | {/* Right hand side */}
120 |
121 |
125 |
129 | {messages.length === 0 && (
130 |
131 | Ask your first question below!
132 |
133 | )}
134 | {messages.map((message, index) => {
135 | const sources = sourcesForMessages[index] || undefined;
136 | const isLastMessage =
137 | !isLoading && index === messages.length - 1;
138 | const previousMessages = index !== messages.length - 1;
139 | return (
140 |
141 |
150 |
151 |
164 |
165 | {message.content}
166 |
167 |
168 | {/* Display the sources */}
169 | {(isLastMessage || previousMessages) && sources && (
170 |
171 | {sources
172 | .filter((source: any, index: number, self: any) => {
173 | const pageNumber =
174 | extractSourcePageNumber(source);
175 | // Check if the current pageNumber is the first occurrence in the array
176 | return (
177 | self.findIndex(
178 | (s: any) =>
179 | extractSourcePageNumber(s) === pageNumber,
180 | ) === index
181 | );
182 | })
183 | .map((source: any) => (
184 |
194 | ))}
195 |
196 | )}
197 |
198 |
199 | );
200 | })}
201 |
202 |
203 |
245 | {error && (
246 |
249 | )}
250 |
251 |
252 |
253 | );
254 | }
255 |
--------------------------------------------------------------------------------
/app/document/[id]/layout.tsx:
--------------------------------------------------------------------------------
1 | import Header from '@/components/ui/Header';
2 |
3 | export default function RootLayout({
4 | children,
5 | }: {
6 | children: React.ReactNode;
7 | }) {
8 | return (
9 |
10 |
11 | {children}
12 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/app/document/[id]/page.tsx:
--------------------------------------------------------------------------------
1 | import prisma from '@/utils/prisma';
2 | import { currentUser } from '@clerk/nextjs';
3 | import type { User } from '@clerk/nextjs/api';
4 | import DocumentClient from './document-client';
5 |
6 | export default async function Page({ params }: { params: { id: string } }) {
7 | const user: User | null = await currentUser();
8 |
9 | const currentDoc = await prisma.document.findFirst({
10 | where: {
11 | id: params.id,
12 | userId: user?.id,
13 | },
14 | });
15 |
16 | if (!currentDoc) {
17 | return This document was not found
;
18 | }
19 |
20 | return (
21 |
22 |
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import '../styles/globals.css';
2 | import { ClerkProvider } from '@clerk/nextjs';
3 | import type { Metadata } from 'next';
4 | import { Anek_Bangla } from 'next/font/google';
5 | import PlausibleProvider from 'next-plausible';
6 |
7 | const anek = Anek_Bangla({
8 | subsets: ['latin'],
9 | display: 'swap',
10 | });
11 |
12 | let title = 'PDF to Chat';
13 | let description = 'Chat with your PDFs in seconds.';
14 | let ogimage = 'https://www.pdftochat.com/og-image.png';
15 | let url = 'https://www.pdftochat.com';
16 | let sitename = 'pdftochat.com';
17 |
18 | export const metadata: Metadata = {
19 | metadataBase: new URL(url),
20 | title,
21 | description,
22 | icons: {
23 | icon: '/favicon.ico',
24 | },
25 | openGraph: {
26 | images: [ogimage],
27 | title,
28 | description,
29 | url: url,
30 | siteName: sitename,
31 | locale: 'en_US',
32 | type: 'website',
33 | },
34 | twitter: {
35 | card: 'summary_large_image',
36 | images: [ogimage],
37 | title,
38 | description,
39 | },
40 | };
41 |
42 | export default function RootLayout({
43 | children,
44 | }: {
45 | children: React.ReactNode;
46 | }) {
47 | return (
48 |
49 |
50 |
51 |
52 |
53 | {children}
54 |
55 |
56 | );
57 | }
58 |
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | import Footer from '@/components/home/Footer';
2 | import Header from '@/components/home/Header';
3 | import Hero from '@/components/home/Hero';
4 | import HowItWorks from '@/components/home/HowItWorks';
5 | import ProudlyOpenSource from '@/components/home/ProudlyOpenSource';
6 | import { currentUser } from '@clerk/nextjs';
7 | import { User } from '@clerk/nextjs/server';
8 | import { redirect } from 'next/navigation';
9 |
10 | export default async function Home() {
11 | const user: User | null = await currentUser();
12 | const isLoggedIn = !!user;
13 | if (isLoggedIn) {
14 | redirect('/dashboard');
15 | }
16 |
17 | return (
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/app/sign-in/[[...sign-in]]/page.tsx:
--------------------------------------------------------------------------------
1 | import Header from '@/components/home/Header';
2 | import { SignIn } from '@clerk/nextjs';
3 |
4 | export default function Page() {
5 | return (
6 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/app/sign-up/[[...sign-up]]/page.tsx:
--------------------------------------------------------------------------------
1 | import Header from '@/components/home/Header';
2 | import { SignUp } from '@clerk/nextjs';
3 |
4 | export default function Page() {
5 | return (
6 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/components/home/Footer.tsx:
--------------------------------------------------------------------------------
1 | import Logo from '../ui/Logo';
2 | import Link from 'next/link';
3 |
4 | const Footer = () => {
5 | return (
6 |
74 | );
75 | };
76 |
77 | export default Footer;
78 |
--------------------------------------------------------------------------------
/components/home/Header.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { useState } from 'react';
3 | import Logo from '../ui/Logo';
4 | import Image from 'next/image';
5 | import Link from 'next/link';
6 |
7 | const Header = () => {
8 | const [open, setOpen] = useState(false);
9 | return (
10 | <>
11 |
12 |
13 |
14 |
18 | Log in
19 |
20 |
24 | Sign up
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | setOpen((i) => !i)}
35 | alt="Menu"
36 | width={20}
37 | height={20}
38 | className="cursor-pointer"
39 | />
40 |
41 |
42 | {open ? (
43 |
44 |
47 |
50 |
51 | ) : null}
52 | >
53 | );
54 | };
55 |
56 | export default Header;
57 |
--------------------------------------------------------------------------------
/components/home/Hero.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 |
3 | const Hero = () => {
4 | return (
5 |
27 | );
28 | };
29 |
30 | export default Hero;
31 |
--------------------------------------------------------------------------------
/components/home/HowItWorks.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image';
2 | import Link from 'next/link';
3 |
4 | const data = [
5 | {
6 | title: 'Sign up',
7 | description: 'Start by signing up for a free PDFtoChat account',
8 | image: '/pen.png',
9 | },
10 | {
11 | title: 'Upload a PDF',
12 | description: 'After login, upload your PDF and let the AI tool analyze it',
13 | image: '/upload.png',
14 | },
15 | {
16 | title: 'Begin Chatting',
17 | description: 'Simply start asking the AI any question about the PDF!',
18 | image: '/chat.png',
19 | },
20 | ];
21 |
22 | const HowItWorks = () => {
23 | return (
24 |
28 |
29 | How it Works
30 |
31 |
32 | {data.map((item, index) => (
33 |
34 |
35 |
42 |
43 |
44 |
45 | {item.title}
46 |
47 |
48 | {item.description}
49 |
50 |
54 |
55 | Get started
56 |
57 |
64 |
65 |
66 |
67 | ))}
68 |
69 |
70 | );
71 | };
72 |
73 | export default HowItWorks;
74 |
--------------------------------------------------------------------------------
/components/home/ProudlyOpenSource.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image';
2 |
3 | const ProudlyOpenSource = () => {
4 | return (
5 |
6 |
7 | Proudly open-source
8 |
9 |
10 | Our source code is available on GitHub - feel free to read, review, or
11 | contribute to it however you want!
12 |
13 |
30 |
31 | );
32 | };
33 |
34 | export default ProudlyOpenSource;
35 |
--------------------------------------------------------------------------------
/components/ui/DocIcon.tsx:
--------------------------------------------------------------------------------
1 | export default function DocIcon() {
2 | return (
3 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/components/ui/Header.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import { UserButton, currentUser } from '@clerk/nextjs';
3 | import { User } from '@clerk/nextjs/server';
4 |
5 | export default async function Header() {
6 | const user: User | null = await currentUser();
7 | const isLoggedIn = !!user;
8 |
9 | return (
10 |
11 |
12 |
31 |
32 |
33 | );
34 | }
35 |
36 | function Logo() {
37 | return (
38 |
97 | );
98 | }
99 |
--------------------------------------------------------------------------------
/components/ui/LoadingDots.tsx:
--------------------------------------------------------------------------------
1 | import styles from '@/styles/loading-dots.module.css';
2 |
3 | const LoadingDots = ({
4 | color = '#000',
5 | style = 'small',
6 | }: {
7 | color: string;
8 | style: string;
9 | }) => {
10 | return (
11 |
12 |
13 |
14 |
15 |
16 | );
17 | };
18 |
19 | export default LoadingDots;
20 |
--------------------------------------------------------------------------------
/components/ui/Logo.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image';
2 | import Link from 'next/link';
3 |
4 | interface LogoProps {
5 | isMobile?: boolean;
6 | }
7 |
8 | const Logo = ({ isMobile }: LogoProps) => {
9 | return (
10 |
11 |
12 |
13 |
20 |
21 | {!isMobile ? (
22 |
23 | PDFtoChat
24 |
25 | ) : null}
26 |
27 |
28 | );
29 | };
30 |
31 | export default Logo;
32 |
--------------------------------------------------------------------------------
/components/ui/TextArea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { cn } from '@/utils/cn';
3 |
4 | export interface TextareaProps
5 | extends React.TextareaHTMLAttributes {}
6 |
7 | const Textarea = React.forwardRef(
8 | ({ className, ...props }, ref) => {
9 | return (
10 |
18 | );
19 | },
20 | );
21 | Textarea.displayName = 'Textarea';
22 |
23 | export { Textarea };
24 |
--------------------------------------------------------------------------------
/components/ui/Toggle.tsx:
--------------------------------------------------------------------------------
1 | import { Switch } from '@headlessui/react';
2 |
3 | function classNames(...classes: any) {
4 | return classes.filter(Boolean).join(' ');
5 | }
6 |
7 | export default function Toggle({ chatOnlyView, setChatOnlyView }: any) {
8 | return (
9 |
10 |
11 |
15 |
20 | PDF + Chat
21 | {' '}
22 |
23 |
31 |
38 |
39 |
43 |
48 | Chat Only
49 | {' '}
50 |
51 |
52 |
53 | );
54 | }
55 |
--------------------------------------------------------------------------------
/middleware.ts:
--------------------------------------------------------------------------------
1 | import { authMiddleware } from '@clerk/nextjs';
2 |
3 | export default authMiddleware({
4 | publicRoutes: ['/'],
5 | });
6 |
7 | export const config = {
8 | matcher: ['/((?!.+\\.[\\w]+$|_next).*)', '/', '/(api|trpc)(.*)'],
9 | };
10 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: true,
4 | images: {
5 | remotePatterns: [
6 | {
7 | protocol: 'https',
8 | hostname: 'img.clerk.com',
9 | port: '',
10 | pathname: '/*',
11 | },
12 | ],
13 | },
14 | experimental: {
15 | esmExternals: 'loose',
16 | },
17 | webpack: (config) => {
18 | config.externals = [...config.externals, { canvas: 'canvas' }];
19 | return config;
20 | },
21 | };
22 |
23 | module.exports = nextConfig;
24 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pdftochat",
3 | "version": "1.0.0",
4 | "private": true,
5 | "license": "MIT",
6 | "scripts": {
7 | "dev": "next dev",
8 | "build": "next build",
9 | "start": "next start",
10 | "vercel-build": "prisma generate && next build",
11 | "prisma:generate": "prisma generate",
12 | "format": "prettier --write .",
13 | "evals": "tsx ./scripts/runEvaluation.ts"
14 | },
15 | "dependencies": {
16 | "@clerk/nextjs": "^4.29.3",
17 | "@headlessui/react": "^1.7.18",
18 | "@langchain/community": "^0.0.50",
19 | "@langchain/core": "^0.1.58",
20 | "@langchain/mongodb": "^0.0.1",
21 | "@langchain/pinecone": "^0.0.4",
22 | "@pinecone-database/pinecone": "^2.0.1",
23 | "@prisma/client": "^5.2.0",
24 | "@radix-ui/react-accordion": "^1.1.1",
25 | "@react-pdf-viewer/core": "3.12.0",
26 | "@react-pdf-viewer/default-layout": "^3.12.0",
27 | "@react-pdf-viewer/page-navigation": "^3.12.0",
28 | "@vercel/postgres": "^0.5.0",
29 | "ai": "^2.1.28",
30 | "clsx": "^1.2.1",
31 | "date-fns": "^2.30.0",
32 | "dotenv": "^16.0.3",
33 | "langchain": "^0.1.34",
34 | "langsmith": "^0.1.18",
35 | "lucide-react": "^0.125.0",
36 | "mongodb": "^6.3.0",
37 | "next": "^13.5.4",
38 | "next-plausible": "^3.12.0",
39 | "pdf-parse": "1.1.1",
40 | "pdfjs-dist": "3.4.120",
41 | "react": "^18.2.0",
42 | "react-dom": "^18.2.0",
43 | "react-markdown": "^8.0.5",
44 | "react-uploader": "^3.43.0",
45 | "tailwind-merge": "^1.10.0",
46 | "tailwindcss": "^3.3.3",
47 | "uploader": "^3.48.3"
48 | },
49 | "devDependencies": {
50 | "@tailwindcss/typography": "^0.5.10",
51 | "@types/node": "^18.14.6",
52 | "@types/react": "^18.0.28",
53 | "@types/react-dom": "^18.0.11",
54 | "@typescript-eslint/parser": "^5.54.0",
55 | "autoprefixer": "^10.4.13",
56 | "encoding": "^0.1.13",
57 | "eslint": "8.35.0",
58 | "eslint-config-next": "13.2.3",
59 | "postcss": "^8.4.21",
60 | "prettier": "^2.8.4",
61 | "prisma": "^5.2.0",
62 | "tsx": "^3.12.3",
63 | "typescript": "^5.2.2"
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | generator client {
2 | provider = "prisma-client-js"
3 | }
4 |
5 | datasource db {
6 | provider = "postgresql"
7 | url = env("POSTGRES_PRISMA_URL") // uses connection pooling
8 | directUrl = env("POSTGRES_URL_NON_POOLING") // uses a direct connection
9 | }
10 |
11 | model Document {
12 | id String @id @default(cuid())
13 | userId String
14 | fileUrl String
15 | fileName String
16 | createdAt DateTime @default(now()) @map(name: "created_at")
17 | }
18 |
--------------------------------------------------------------------------------
/public/align-justify.svg:
--------------------------------------------------------------------------------
1 |
9 |
--------------------------------------------------------------------------------
/public/bot-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nutlope/pdftochat/6c26c3e16f78f8466f9c6b35e18840b1bdb326fb/public/bot-icon.png
--------------------------------------------------------------------------------
/public/chat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nutlope/pdftochat/6c26c3e16f78f8466f9c6b35e18840b1bdb326fb/public/chat.png
--------------------------------------------------------------------------------
/public/custom-chat-bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nutlope/pdftochat/6c26c3e16f78f8466f9c6b35e18840b1bdb326fb/public/custom-chat-bg.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nutlope/pdftochat/6c26c3e16f78f8466f9c6b35e18840b1bdb326fb/public/favicon.ico
--------------------------------------------------------------------------------
/public/github.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nutlope/pdftochat/6c26c3e16f78f8466f9c6b35e18840b1bdb326fb/public/github.png
--------------------------------------------------------------------------------
/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nutlope/pdftochat/6c26c3e16f78f8466f9c6b35e18840b1bdb326fb/public/logo.png
--------------------------------------------------------------------------------
/public/og-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nutlope/pdftochat/6c26c3e16f78f8466f9c6b35e18840b1bdb326fb/public/og-image.png
--------------------------------------------------------------------------------
/public/pen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nutlope/pdftochat/6c26c3e16f78f8466f9c6b35e18840b1bdb326fb/public/pen.png
--------------------------------------------------------------------------------
/public/profile-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nutlope/pdftochat/6c26c3e16f78f8466f9c6b35e18840b1bdb326fb/public/profile-icon.png
--------------------------------------------------------------------------------
/public/right-arrow.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/upload.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nutlope/pdftochat/6c26c3e16f78f8466f9c6b35e18840b1bdb326fb/public/upload.png
--------------------------------------------------------------------------------
/public/user.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/public/usericon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nutlope/pdftochat/6c26c3e16f78f8466f9c6b35e18840b1bdb326fb/public/usericon.png
--------------------------------------------------------------------------------
/styles/globals.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Anek+Bangla:wght@100;200;300;400;500;600;700;800&display=swap');
2 |
3 | @tailwind base;
4 | @tailwind components;
5 | @tailwind utilities;
6 |
7 | @layer utilities {
8 | /* Hide scrollbar for Chrome, Safari and Opera */
9 | .no-scrollbar::-webkit-scrollbar {
10 | display: none;
11 | }
12 | /* Hide scrollbar for IE, Edge and Firefox */
13 | .no-scrollbar {
14 | -ms-overflow-style: none; /* IE and Edge */
15 | scrollbar-width: none; /* Firefox */
16 | }
17 | }
18 |
19 | * {
20 | font-family: 'Anek Bangla', sans-serif;
21 | }
22 |
23 | .shadows {
24 | text-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
25 | }
26 |
27 | .bg_linear {
28 | background: linear-gradient(
29 | 95deg,
30 | #101023 5.97%,
31 | rgba(15, 15, 33, 0.97) 52.2%,
32 | #0e0e20 91.78%
33 | );
34 | }
35 | .bg_linear_gpt {
36 | background: linear-gradient(
37 | 85deg,
38 | rgba(252, 252, 252, 0.44) 1.85%,
39 | rgba(242, 243, 248, 0.53) 89.3%
40 | );
41 | }
42 | .drop_shadow {
43 | filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.2));
44 | }
45 |
46 | .text_bg {
47 | background: linear-gradient(
48 | 90deg,
49 | #101023 10.71%,
50 | rgba(15, 15, 30, 0.64) 50.71%,
51 | rgba(13, 13, 25, 0.84) 88.54%
52 | );
53 | background-clip: text;
54 | -webkit-background-clip: text;
55 | -webkit-text-fill-color: transparent;
56 | }
57 |
58 | .custom_bg {
59 | background: linear-gradient(
60 | 0deg,
61 | #fcfcfc 0.27%,
62 | rgba(242, 243, 248, 0.3) 99.75%
63 | ),
64 | url('/custom-chat-bg.png'), lightgray 0% 0% / 100px 100px repeat;
65 | }
66 |
67 | /* //scroll bar */
68 | /* width */
69 | ::-webkit-scrollbar {
70 | width: 12px;
71 | }
72 |
73 | /* Track */
74 | ::-webkit-scrollbar-track {
75 | background: #f1f1f1;
76 | border: 1px solid #d9d9d9;
77 | }
78 |
79 | /* Handle */
80 | ::-webkit-scrollbar-thumb {
81 | background: #d9d9d9;
82 | }
83 |
84 | /* Handle on hover */
85 | ::-webkit-scrollbar-thumb:hover {
86 | background: #d9d9d9;
87 | }
88 |
--------------------------------------------------------------------------------
/styles/loading-dots.module.css:
--------------------------------------------------------------------------------
1 | .loading {
2 | display: inline-flex;
3 | align-items: center;
4 | }
5 |
6 | .loading .spacer {
7 | margin-right: 2px;
8 | }
9 |
10 | .loading span {
11 | animation-name: blink;
12 | animation-duration: 1.4s;
13 | animation-iteration-count: infinite;
14 | animation-fill-mode: both;
15 | width: 5px;
16 | height: 5px;
17 | border-radius: 50%;
18 | display: inline-block;
19 | margin: 0 1px;
20 | }
21 |
22 | .loading span:nth-of-type(2) {
23 | animation-delay: 0.2s;
24 | }
25 |
26 | .loading span:nth-of-type(3) {
27 | animation-delay: 0.4s;
28 | }
29 |
30 | .loading2 {
31 | display: inline-flex;
32 | align-items: center;
33 | }
34 |
35 | .loading2 .spacer {
36 | margin-right: 2px;
37 | }
38 |
39 | .loading2 span {
40 | animation-name: blink;
41 | animation-duration: 1.4s;
42 | animation-iteration-count: infinite;
43 | animation-fill-mode: both;
44 | width: 4px;
45 | height: 4px;
46 | border-radius: 50%;
47 | display: inline-block;
48 | margin: 0 1px;
49 | }
50 |
51 | .loading2 span:nth-of-type(2) {
52 | animation-delay: 0.2s;
53 | }
54 |
55 | .loading2 span:nth-of-type(3) {
56 | animation-delay: 0.4s;
57 | }
58 |
59 | @keyframes blink {
60 | 0% {
61 | opacity: 0.2;
62 | }
63 | 20% {
64 | opacity: 1;
65 | }
66 | 100% {
67 | opacity: 0.2;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: [
4 | './app/**/*.{js,ts,jsx,tsx}',
5 | './pages/**/*.{js,ts,jsx,tsx}',
6 | './components/**/*.{js,ts,jsx,tsx}',
7 | ],
8 | theme: {
9 | extend: {},
10 | },
11 | plugins: [require('@tailwindcss/typography')],
12 | };
13 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "bundler",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true,
17 | "plugins": [
18 | {
19 | "name": "next"
20 | }
21 | ],
22 | "paths": {
23 | "@/*": ["./*"]
24 | }
25 | },
26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
27 | "exclude": ["node_modules"]
28 | }
29 |
--------------------------------------------------------------------------------
/utils/chatType.ts:
--------------------------------------------------------------------------------
1 | import { Document } from 'langchain/document';
2 |
3 | export type Message = {
4 | type: 'apiMessage' | 'userMessage';
5 | message: string;
6 | isStreaming?: boolean;
7 | sourceDocs?: Document[];
8 | };
9 |
--------------------------------------------------------------------------------
/utils/cn.ts:
--------------------------------------------------------------------------------
1 | import { ClassValue, clsx } from 'clsx';
2 | import { twMerge } from 'tailwind-merge';
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
--------------------------------------------------------------------------------
/utils/config.ts:
--------------------------------------------------------------------------------
1 | export const CONDENSE_TEMPLATE = `Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.
2 |
3 | Chat History:
4 | {chat_history}
5 | Follow Up Input: {question}
6 | Standalone question:`;
7 |
8 | export const QA_TEMPLATE = `You are a helpful AI assistant. Use the following pieces of context to answer the question at the end.
9 | If you don't know the answer, just say you don't know. DO NOT try to make up an answer.
10 | If the question is not related to the context, politely respond that you are tuned to only answer questions that are related to the context.
11 |
12 | {context}
13 |
14 | Question: {question}
15 | Please return an answer in markdown with clear headings and lists:`;
16 |
--------------------------------------------------------------------------------
/utils/prisma.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from '@prisma/client';
2 |
3 | declare global {
4 | var prisma: PrismaClient | undefined;
5 | }
6 |
7 | const client = globalThis.prisma || new PrismaClient();
8 | if (process.env.NODE_ENV !== 'production') globalThis.prisma = client;
9 |
10 | export default client;
11 |
--------------------------------------------------------------------------------
/utils/ragChain.ts:
--------------------------------------------------------------------------------
1 | import { createStuffDocumentsChain } from 'langchain/chains/combine_documents';
2 | import { createRetrievalChain } from 'langchain/chains/retrieval';
3 | import { createHistoryAwareRetriever } from 'langchain/chains/history_aware_retriever';
4 | import { BaseLanguageModel } from '@langchain/core/language_models/base';
5 | import { BaseRetriever } from '@langchain/core/retrievers';
6 | import {
7 | ChatPromptTemplate,
8 | MessagesPlaceholder,
9 | } from '@langchain/core/prompts';
10 |
11 | import type { Runnable } from '@langchain/core/runnables';
12 | import type { BaseMessage } from '@langchain/core/messages';
13 |
14 | const historyAwarePrompt = ChatPromptTemplate.fromMessages([
15 | new MessagesPlaceholder('chat_history'),
16 | ['user', '{input}'],
17 | [
18 | 'user',
19 | 'Given the above conversation, generate a concise vector store search query to look up in order to get information relevant to the conversation.',
20 | ],
21 | ]);
22 |
23 | const ANSWER_SYSTEM_TEMPLATE = `You are a helpful AI assistant. Use the following pieces of context to answer the question at the end.
24 | If you don't know the answer, just say you don't know. DO NOT try to make up an answer.
25 | If the question is not related to the context, politely respond that you are tuned to only answer questions that are related to the context.
26 |
27 |
28 | {context}
29 |
30 |
31 | Please return your answer in markdown with clear headings and lists.`;
32 |
33 | const answerPrompt = ChatPromptTemplate.fromMessages([
34 | ['system', ANSWER_SYSTEM_TEMPLATE],
35 | new MessagesPlaceholder('chat_history'),
36 | ['user', '{input}'],
37 | ]);
38 |
39 | export async function createRAGChain(
40 | model: BaseLanguageModel,
41 | retriever: BaseRetriever,
42 | ): Promise> {
43 | // Create a chain that can rephrase incoming questions for the retriever,
44 | // taking previous chat history into account. Returns relevant documents.
45 | const historyAwareRetrieverChain = await createHistoryAwareRetriever({
46 | llm: model,
47 | retriever,
48 | rephrasePrompt: historyAwarePrompt,
49 | });
50 |
51 | // Create a chain that answers questions using retrieved relevant documents as context.
52 | const documentChain = await createStuffDocumentsChain({
53 | llm: model,
54 | prompt: answerPrompt,
55 | });
56 |
57 | // Create a chain that combines the above retriever and question answering chains.
58 | const conversationalRetrievalChain = await createRetrievalChain({
59 | retriever: historyAwareRetrieverChain,
60 | combineDocsChain: documentChain,
61 | });
62 |
63 | // "Pick" the answer from the retrieval chain output object.
64 | return conversationalRetrievalChain.pick('answer');
65 | }
66 |
--------------------------------------------------------------------------------