├── .env.example ├── .gitignore ├── LICENSE ├── README.md ├── biome.json ├── components.json ├── next.config.ts ├── package.json ├── pnpm-lock.yaml ├── postcss.config.mjs ├── prisma ├── migrations │ ├── 0_init │ │ └── migration.sql │ ├── 20241118143807_add_oai_table │ │ └── migration.sql │ └── 20241118144227_add_dummy_field_to_oai │ │ └── migration.sql └── schema.prisma ├── public ├── file.svg ├── globe.svg ├── next.svg ├── nextrag.png ├── tech │ ├── next.svg │ ├── next.webp │ ├── postgres.png │ └── prisma.svg ├── vercel.svg └── window.svg ├── src ├── app │ ├── (chat) │ │ ├── chat │ │ │ └── page.tsx │ │ └── layout.tsx │ ├── (docs) │ │ ├── docs │ │ │ └── page.tsx │ │ └── layout.tsx │ ├── api │ │ ├── chat │ │ │ └── route.ts │ │ ├── healthcheck │ │ │ └── route.ts │ │ ├── inngest │ │ │ ├── events │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ └── search │ │ │ └── route.ts │ ├── favicon.ico │ ├── fonts │ │ ├── GeistMonoVF.woff │ │ └── GeistVF.woff │ ├── globals.css │ ├── layout.tsx │ ├── opengraph-image.png │ ├── page.tsx │ └── twitter-image.png ├── components │ ├── navbar.tsx │ ├── tabs │ │ ├── chat-tab.tsx │ │ ├── ingest-tab.tsx │ │ └── search-tab.tsx │ ├── tech-icons.tsx │ ├── theme-provider.tsx │ ├── theme-toggle.tsx │ └── ui │ │ ├── alert.tsx │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── code-block.tsx │ │ ├── dot-pattern.tsx │ │ ├── dropdown-menu.tsx │ │ ├── input.tsx │ │ ├── scroll-area.tsx │ │ ├── sonner.tsx │ │ ├── tabs.tsx │ │ └── textarea.tsx ├── inngest │ ├── client.ts │ └── functions │ │ ├── embedding.ts │ │ └── retrieval.ts └── lib │ ├── actions │ └── search.ts │ ├── db │ ├── client.ts │ ├── config.ts │ ├── pg.ts │ └── vector.ts │ ├── env.mjs │ └── utils.ts ├── tailwind.config.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | # For the LLM of your choice, e.g. OpenAI, based on the Vercel AI SDK 2 | OPENAI_API_KEY=... 3 | 4 | # For Prisma 5 | POSTGRES_URL=... 6 | 7 | # For node-pg 8 | PGUSER=... 9 | PGPASSWORD=... 10 | PGHOST=... 11 | PGPORT=... 12 | PGDATABASE=... 13 | -------------------------------------------------------------------------------- /.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.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | 32 | # env files (can opt-in for committing if needed) 33 | !env.example 34 | .env 35 | .env.local 36 | 37 | # vercel 38 | .vercel 39 | 40 | # typescript 41 | *.tsbuildinfo 42 | next-env.d.ts 43 | 44 | # postgres 45 | postgres_data 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Hamed Mohammadpour 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NextRag: Next.js RAG with PGVector 2 | 3 | ![NextRAG Demo](./public/nextrag.png) 4 | A production-ready implementation of Retrieval Augmented Generation (RAG) using Next.js, PostgreSQL + pgvector (with `node-pg`), Prisma, and Vercel AI SDK. 5 | 6 | ## Introduction 7 | 8 | This project demonstrates how to implement RAG (Retrieval Augmented Generation) using PostgreSQL's vector similarity search capabilities. It's designed as a reference implementation that you can adapt for your specific use case. 9 | 10 | ### Key Concepts 11 | 12 | 1. **Vector Embeddings**: Text is converted into high-dimensional vectors that capture semantic meaning using OpenAI's embedding models. 13 | 14 | 2. **Similarity Search**: pgvector enables efficient similarity search between these vectors, helping find relevant content. 15 | 16 | 3. **Chunking Strategies**: Documents are broken down into manageable pieces using different strategies (sentence, paragraph, or fixed-size) to optimize retrieval. 17 | 18 | 4. **Metadata Tracking**: Each chunk maintains metadata about its source, creation time, and relationship to other chunks. 19 | 20 | 5. **Background Processing**: Long-running tasks like document ingestion are handled asynchronously using Inngest. 21 | 22 | ### Sample Implementation 23 | 24 | This codebase provides: 25 | 26 | - A flexible `VectorDB` class for vector operations 27 | - Multiple chunking strategies 28 | - Configurable similarity metrics 29 | - Type-safe database operations 30 | - RAG-powered chat interface 31 | 32 | ## Features 33 | 34 | - 🔍 Semantic search with pgvector 35 | - 🤖 RAG-powered chat interface 36 | - 📝 Multiple text chunking strategies 37 | - 🔄 Background processing with Inngest 38 | - 🎯 Flexible similarity metrics 39 | - 📊 Rich metadata support 40 | - 🔒 Type-safe database operations 41 | 42 | ## Tech Stack 43 | 44 | - **Next.js 15** - React framework 45 | - **PostgreSQL + pgvector** - Vector similarity search 46 | - **Vercel AI SDK** - AI/LLM utilities 47 | - **Prisma** - Type-safe database schema 48 | - **node-pg** - SQL query sanitization 49 | - **Inngest** - Background job processing 50 | - **OpenAI** - Embeddings and chat completion 51 | - **Tailwind CSS** - Styling 52 | - **TypeScript** - Type safety 53 | 54 | ## Quick Start 55 | 56 | ## 1. **Clone and Install** 57 | 58 | ```bash 59 | git clone https://github.com/hamedmp/nextrag 60 | cd nextrag 61 | pnpm install 62 | ``` 63 | 64 | ## 2. **Environment Setup** 65 | 66 | You need environment variables for the LLM of your choice and the Database 67 | 68 | ```bash 69 | cp .env.example .env 70 | ``` 71 | 72 | Required environment variables: 73 | 74 | ```bash 75 | # Database (Vercel Postgres or Neon) 76 | POSTGRES_URL="postgres://..." 77 | 78 | # OpenAI 79 | OPENAI_API_KEY="sk-..." 80 | 81 | # node-pg 82 | PGUSER=... 83 | PGPASSWORD=... 84 | PGHOST=... 85 | PGPORT=... 86 | PGDATABASE=... 87 | 88 | # Inngest (optional, for background jobs) 89 | INNGEST_EVENT_KEY="..." 90 | INNGEST_SIGNING_KEY="..." 91 | ``` 92 | 93 | ## 3. **Database Setup** 94 | 95 | ### Option 1: Enable pgvector extension manually 96 | 97 | Enable pgvector extension in your PostgreSQL database: 98 | 99 | ```sql 100 | CREATE EXTENSION IF NOT EXISTS vector; 101 | ``` 102 | 103 | ### Option 2: Enable pgvector extension with Prisma migrations 104 | 105 | Alternatively, you can do it with the generated Prisma migrations: 106 | 107 | Prisma doesn't natively support pgvector's vector type, but we can use the `Unsupported` scalar: 108 | 109 | ```prisma 110 | generator client { 111 | provider = "prisma-client-js" 112 | } 113 | 114 | datasource db { 115 | provider = "postgresql" 116 | url = env("POSTGRES_URL") 117 | } 118 | 119 | model documents { 120 | id BigInt @id @default(autoincrement()) 121 | content String? 122 | embedding Unsupported("vector")? 123 | metadata Json? @default("{}") 124 | createdAt DateTime @default(now()) 125 | updatedAt DateTime @updatedAt 126 | } 127 | ``` 128 | 129 | #### Migrations 130 | 131 | 1. Create a migration: 132 | 133 | ```bash 134 | pnpm prisma migrate dev --name add_vector_support 135 | ``` 136 | 137 | 2. In the generated migration file, add pgvector setup: 138 | 139 | ```sql 140 | -- Enable pgvector extension 141 | CREATE EXTENSION IF NOT EXISTS vector; 142 | 143 | -- CreateTable 144 | CREATE TABLE "documents" ( 145 | "id" BIGSERIAL NOT NULL, 146 | "content" TEXT, 147 | "embedding" vector(1536), 148 | "metadata" JSONB DEFAULT '{}', 149 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 150 | "updatedAt" TIMESTAMP(3) NOT NULL, 151 | CONSTRAINT "documents_pkey" PRIMARY KEY ("id") 152 | ); 153 | 154 | -- Create HNSW index for faster similarity search 155 | CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops); 156 | ``` 157 | 158 | 3. Apply the migration: 159 | 160 | ```bash 161 | pnpm prisma migrate deploy 162 | ``` 163 | 164 | ## 4. **Run Development Server and Inngest server** 165 | 166 | In a separate terminal, run the Inngest server: 167 | 168 | ```bash 169 | pnpm run inngest 170 | ``` 171 | 172 | The server will start on `http://127.0.0.1:8288`. 173 | 174 | In another separate terminal, run the Next.js development server: 175 | 176 | ```bash 177 | pnpm dev 178 | ``` 179 | 180 | Visit `http://localhost:3000` to see the app. 181 | 182 | ## VectorDB Usage 183 | 184 | The `VectorDB` class provides a flexible interface for vector operations: 185 | 186 | ```typescript 187 | // Initialize with table configuration 188 | const vectorDB = new VectorDB( 189 | { 190 | tableName: 'documents', 191 | columns: { 192 | id: 'id', 193 | vector: 'embedding', 194 | content: 'text', 195 | metadata: 'metadata', 196 | createdAt: 'createdAt', 197 | }, 198 | }, 199 | { 200 | // Optional custom configuration 201 | embedding: { 202 | model: 'text-embedding-3-small', 203 | dimensions: 1536, 204 | distance: 'cosine', 205 | }, 206 | chunking: { 207 | method: 'paragraph', 208 | fixedSize: 500, 209 | }, 210 | search: { 211 | defaultLimit: 5, 212 | }, 213 | } 214 | ); 215 | 216 | // Add text with chunking and metadata 217 | await vectorDB.addText('Your content here', { 218 | chunkingMethod: 'paragraph', 219 | metadata: { 220 | source: 'documentation', 221 | category: 'setup', 222 | }, 223 | }); 224 | 225 | // Search with filters and custom options 226 | const results = await vectorDB.searchSimilar('your query', { 227 | limit: 10, 228 | distance: 'cosine', 229 | filter: { category: 'setup' }, 230 | select: ['content', 'metadata->category as category'], 231 | }); 232 | ``` 233 | 234 | ## Chunking Methods 235 | 236 | Three text chunking strategies are available: 237 | 238 | ```typescript 239 | // 1. Sentence-based chunking 240 | const chunks = vectorDB.chunkText(text, 'sentence'); 241 | 242 | // 2. Paragraph-based chunking (split by double newline) 243 | const chunks = vectorDB.chunkText(text, 'paragraph'); 244 | 245 | // 3. Fixed-size chunking (with word boundaries) 246 | const chunks = vectorDB.chunkText(text, 'fixed'); 247 | ``` 248 | 249 | ## Similarity Metrics 250 | 251 | PGVector supports multiple similarity metrics: 252 | 253 | ```typescript 254 | // Cosine similarity (normalized, recommended for OpenAI embeddings) 255 | await vectorDB.searchSimilar(query, { distance: 'cosine' }); 256 | 257 | // Euclidean distance 258 | await vectorDB.searchSimilar(query, { distance: 'euclidean' }); 259 | 260 | // Inner product 261 | await vectorDB.searchSimilar(query, { distance: 'inner_product' }); 262 | ``` 263 | 264 | ## Project Structure 265 | 266 | ``` 267 | src/ 268 | ├── app/ # Next.js App Router pages 269 | │ ├── (chat)/ # Playground 270 | │ ├── api/ # API routes 271 | │ └── docs/ # Documentation pages 272 | ├── components/ # UI components 273 | ├── lib/ 274 | │ ├── db/ # Database utilities 275 | │ │ ├── vector.ts # VectorDB class 276 | │ │ └── config.ts # Configuration 277 | │ └── actions/ # Server actions 278 | └── inngest/ # Background jobs 279 | ``` 280 | 281 | ## Development 282 | 283 | ### Prerequisites 284 | 285 | - Node.js 18+ 286 | - PostgreSQL 15+ with pgvector extension 287 | - OpenAI API key 288 | - Vercel account (for deployment) 289 | 290 | ### Database Indexes 291 | 292 | For better search performance, create appropriate indexes: 293 | 294 | ```sql 295 | -- For cosine similarity (recommended) 296 | CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops); 297 | 298 | -- For L2 distance 299 | CREATE INDEX ON documents USING hnsw (embedding vector_l2_ops); 300 | ``` 301 | 302 | ## Contributing 303 | 304 | 1. Fork the repository 305 | 2. Create your feature branch (`git checkout -b feature/amazing-feature`) 306 | 3. Commit your changes (`git commit -m 'Add amazing feature'`) 307 | 4. Push to the branch (`git push origin feature/amazing-feature`) 308 | 5. Open a Pull Request 309 | 310 | ## License 311 | 312 | MIT License - see [LICENSE](LICENSE) file for details. 313 | 314 | ## Acknowledgments 315 | 316 | - [Vercel AI SDK](https://sdk.vercel.ai/docs) 317 | - [pgvector](https://github.com/pgvector/pgvector) 318 | - [Inngest](https://www.inngest.com/) 319 | - [Prisma](https://www.prisma.io/) 320 | 321 | --- 322 | 323 | Built with ❤️ using Next.js and pgvector 324 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", 3 | "vcs": { "enabled": false, "clientKind": "git", "useIgnoreFile": false }, 4 | "files": { "ignoreUnknown": false, "ignore": [] }, 5 | "formatter": { "enabled": true, "indentStyle": "tab" }, 6 | "organizeImports": { "enabled": true }, 7 | "linter": { 8 | "enabled": true, 9 | "rules": { 10 | "recommended": false, 11 | "complexity": { 12 | "noUselessThisAlias": "error", 13 | "noUselessTypeConstraint": "error" 14 | }, 15 | "correctness": { 16 | "noUnusedVariables": "error", 17 | "useArrayLiterals": "off" 18 | }, 19 | "style": { "noNamespace": "error", "useAsConstAssertion": "error" }, 20 | "suspicious": { 21 | "noExplicitAny": "error", 22 | "noExtraNonNullAssertion": "error", 23 | "noMisleadingInstantiator": "error", 24 | "noUnsafeDeclarationMerging": "error", 25 | "useNamespaceKeyword": "error" 26 | } 27 | } 28 | }, 29 | "javascript": { "formatter": { "quoteStyle": "double" } }, 30 | "overrides": [ 31 | { 32 | "include": ["*.ts", "*.tsx", "*.mts", "*.cts"], 33 | "linter": { 34 | "rules": { 35 | "correctness": { 36 | "noConstAssign": "off", 37 | "noGlobalObjectCalls": "off", 38 | "noInvalidBuiltinInstantiation": "off", 39 | "noInvalidConstructorSuper": "off", 40 | "noNewSymbol": "off", 41 | "noSetterReturn": "off", 42 | "noUndeclaredVariables": "off", 43 | "noUnreachable": "off", 44 | "noUnreachableSuper": "off" 45 | }, 46 | "style": { 47 | "noArguments": "error", 48 | "noVar": "error", 49 | "useConst": "error" 50 | }, 51 | "suspicious": { 52 | "noClassAssign": "off", 53 | "noDuplicateClassMembers": "off", 54 | "noDuplicateObjectKeys": "off", 55 | "noDuplicateParameters": "off", 56 | "noFunctionAssign": "off", 57 | "noImportAssign": "off", 58 | "noRedeclare": "off", 59 | "noUnsafeNegation": "off", 60 | "useGetterReturn": "off" 61 | } 62 | } 63 | } 64 | }, 65 | { 66 | "include": ["*.ts", "*.tsx", "*.mts", "*.cts"], 67 | "linter": { 68 | "rules": { 69 | "correctness": { 70 | "noConstAssign": "off", 71 | "noGlobalObjectCalls": "off", 72 | "noInvalidBuiltinInstantiation": "off", 73 | "noInvalidConstructorSuper": "off", 74 | "noNewSymbol": "off", 75 | "noSetterReturn": "off", 76 | "noUndeclaredVariables": "off", 77 | "noUnreachable": "off", 78 | "noUnreachableSuper": "off" 79 | }, 80 | "style": { 81 | "noArguments": "error", 82 | "noVar": "error", 83 | "useConst": "error" 84 | }, 85 | "suspicious": { 86 | "noClassAssign": "off", 87 | "noDuplicateClassMembers": "off", 88 | "noDuplicateObjectKeys": "off", 89 | "noDuplicateParameters": "off", 90 | "noFunctionAssign": "off", 91 | "noImportAssign": "off", 92 | "noRedeclare": "off", 93 | "noUnsafeNegation": "off", 94 | "useGetterReturn": "off" 95 | } 96 | } 97 | } 98 | } 99 | ] 100 | } 101 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from 'next'; 2 | 3 | const nextConfig: NextConfig = { 4 | typescript: { 5 | ignoreBuildErrors: true, 6 | }, 7 | }; 8 | 9 | export default nextConfig; 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-rag-postgres", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbopack", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "inngest": "pnpm dlx inngest-cli@latest dev --no-discovery -u localhost:3000/api/inngest" 11 | }, 12 | "dependencies": { 13 | "@ai-sdk/openai": "^0.0.72", 14 | "@prisma/client": "5.22.0", 15 | "@radix-ui/react-dropdown-menu": "^2.1.2", 16 | "@radix-ui/react-scroll-area": "^1.2.1", 17 | "@radix-ui/react-slot": "^1.1.0", 18 | "@radix-ui/react-tabs": "^1.1.1", 19 | "@t3-oss/env-nextjs": "^0.11.1", 20 | "@tailwindcss/typography": "^0.5.15", 21 | "ai": "^3.4.33", 22 | "class-variance-authority": "^0.7.0", 23 | "clsx": "^2.1.1", 24 | "framer-motion": "^11.11.17", 25 | "geist": "^1.3.1", 26 | "inngest": "^3.25.1", 27 | "lucide-react": "^0.460.0", 28 | "next": "15.0.3", 29 | "next-themes": "^0.4.3", 30 | "pg": "^8.13.1", 31 | "react": "19.0.0-rc-66855b96-20241106", 32 | "react-dom": "19.0.0-rc-66855b96-20241106", 33 | "react-markdown": "^9.0.1", 34 | "react-syntax-highlighter": "^15.6.1", 35 | "sonner": "^1.7.0", 36 | "tailwind-merge": "^2.5.4", 37 | "tailwindcss-animate": "^1.0.7", 38 | "zod": "^3.23.8" 39 | }, 40 | "devDependencies": { 41 | "@biomejs/biome": "1.9.4", 42 | "@types/node": "^20", 43 | "@types/pg": "^8.11.10", 44 | "@types/react": "^18", 45 | "@types/react-dom": "^18", 46 | "@types/react-syntax-highlighter": "^15.5.13", 47 | "eslint": "^8", 48 | "eslint-config-next": "15.0.3", 49 | "postcss": "^8", 50 | "prisma": "^5.22.0", 51 | "tailwindcss": "^3.4.1", 52 | "typescript": "^5" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /prisma/migrations/0_init/migration.sql: -------------------------------------------------------------------------------- 1 | CREATE EXTENSION IF NOT EXISTS vector; 2 | 3 | -- CreateTable 4 | CREATE TABLE "items" ( 5 | "id" BIGSERIAL NOT NULL, 6 | "embedding" vector, 7 | 8 | CONSTRAINT "items_pkey" PRIMARY KEY ("id") 9 | ); -------------------------------------------------------------------------------- /prisma/migrations/20241118143807_add_oai_table/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "oai" ( 3 | "id" BIGSERIAL NOT NULL, 4 | "chunk" TEXT, 5 | "embedding" vector, 6 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 7 | "metadata" JSONB DEFAULT '{}', 8 | "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 9 | 10 | CONSTRAINT "oai_pkey" PRIMARY KEY ("id") 11 | ); 12 | -------------------------------------------------------------------------------- /prisma/migrations/20241118144227_add_dummy_field_to_oai/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "oai" ADD COLUMN "dummy_field" TEXT; 3 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | } 4 | 5 | datasource db { 6 | provider = "postgresql" 7 | url = env("POSTGRES_URL") 8 | } 9 | 10 | model items { 11 | id BigInt @id @default(autoincrement()) 12 | embedding Unsupported("vector")? 13 | } 14 | 15 | model oai { 16 | id BigInt @id @default(autoincrement()) 17 | chunk String? 18 | embedding Unsupported("vector")? 19 | createdAt DateTime @default(now()) 20 | metadata Json? @default("{}") 21 | updatedAt DateTime @default(now()) 22 | dummy_field String? 23 | } 24 | -------------------------------------------------------------------------------- /public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/nextrag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HamedMP/NextRag/2c7e85018220e7b001710a28b88d27338d9327e6/public/nextrag.png -------------------------------------------------------------------------------- /public/tech/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/tech/next.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HamedMP/NextRag/2c7e85018220e7b001710a28b88d27338d9327e6/public/tech/next.webp -------------------------------------------------------------------------------- /public/tech/postgres.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HamedMP/NextRag/2c7e85018220e7b001710a28b88d27338d9327e6/public/tech/postgres.png -------------------------------------------------------------------------------- /public/tech/prisma.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/(chat)/chat/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; 4 | import { IngestTab } from "@/components/tabs/ingest-tab"; 5 | import { SearchTab } from "@/components/tabs/search-tab"; 6 | import { ChatTab } from "@/components/tabs/chat-tab"; 7 | import { Card } from "@/components/ui/card"; 8 | import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; 9 | import { Lightbulb, AlertTriangle } from "lucide-react"; 10 | 11 | export default function PlaygroundPage() { 12 | const isProduction = process.env.NODE_ENV === "production"; 13 | 14 | if (isProduction) { 15 | return ( 16 |
17 | 18 | 19 | Demo Environment Only 20 | 21 | This playground is configured to work only in development 22 | environment with your local database setup. To use this feature, 23 | please run the application locally after setting up your own 24 | database and environment variables. 25 | 26 | 27 |
28 | ); 29 | } 30 | 31 | return ( 32 |
33 |
34 |

RAG Playground

35 |

36 | Experiment with vector search and RAG capabilities in this interactive 37 | playground. Upload documents, test similarity search, and chat with 38 | your data. 39 |

40 |
41 | 42 | 43 | 44 | Local Development Only 45 | 46 | This playground works with your local database setup. Make sure you 47 | have configured your environment variables and database connections 48 | correctly before using these features. 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | Ingest Data 57 | 58 | 59 | Search 60 | 61 | 62 | Chat 63 | 64 | 65 | 66 |
67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 |
79 |
80 |
81 | 82 |
83 |

84 | Note: This is a development environment playground. For production 85 | use, please deploy your own instance with proper database 86 | configuration. 87 |

88 |
89 |
90 | ); 91 | } 92 | -------------------------------------------------------------------------------- /src/app/(chat)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Navbar } from "@/components/navbar"; 2 | 3 | export default function PlaygroundLayout({ 4 | children, 5 | }: { 6 | children: React.ReactNode; 7 | }) { 8 | return ( 9 |
10 | 11 |
{children}
12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/app/(docs)/docs/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Card } from "@/components/ui/card"; 4 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; 5 | import { CodeBlock } from "@/components/ui/code-block"; 6 | import { AlertCircle } from "lucide-react"; 7 | import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; 8 | import { Badge } from "@/components/ui/badge"; 9 | 10 | export default function DocsPage() { 11 | return ( 12 |
13 |
14 |

Documentation

15 |

16 | Learn how to integrate vector search and RAG capabilities into your 17 | Next.js application. 18 |

19 |
20 | 21 | 22 | 23 | Quick Start 24 | 25 | Follow the setup instructions below to get started with vector search 26 | in minutes. Make sure you have PostgreSQL with pgvector extension 27 | enabled. 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | Setup 36 | 37 | 38 | PGVector 39 | 40 | 41 | VectorDB 42 | 43 | 44 | Examples 45 | 46 | 47 | 48 |
49 | 50 |
51 |

52 | Getting Started 53 |

54 |

55 | Complete these steps to set up vector search in your 56 | application. 57 |

58 |
59 | 60 |
61 |
62 |
63 | Step 1 64 |

Database Setup

65 |
66 |
67 |

Create a Postgres database with either:

68 |
    69 |
  • Vercel Postgres (recommended for quick start)
  • 70 |
  • NeonDB (recommended for production)
  • 71 |
72 |
73 |
74 | 75 |
76 |
77 | Step 2 78 |

79 | Environment Variables 80 |

81 |
82 |
83 | {` 84 | # Database 85 | POSTGRES_URL="postgres://..." 86 | 87 | # OpenAI 88 | OPENAI_API_KEY="sk-..." 89 | 90 | # Inngest (optional, for background jobs) 91 | INNGEST_EVENT_KEY="..." 92 | INNGEST_SIGNING_KEY="..." 93 | `} 94 |
95 |
96 | 97 |
98 |
99 | Step 3 100 |

Enable pgvector

101 |
102 |
103 |

Connect to your database and run:

104 | {`CREATE EXTENSION vector;`} 105 |
106 |
107 | 108 |
109 |
110 | Step 4 111 |

Create Tables

112 |
113 |
114 |

Using Prisma schema:

115 | {` 116 | model items { 117 | id BigInt @id @default(autoincrement()) 118 | embedding Unsupported("vector")? 119 | metadata Json? @default("{}") 120 | createdAt DateTime @default(now()) 121 | updatedAt DateTime @updatedAt 122 | } 123 | `} 124 |
125 |
126 |
127 |
128 | 129 | 130 |
131 |

132 | PGVector Setup 133 |

134 |

135 | Configure and optimize your vector database for similarity 136 | search. 137 |

138 |
139 | 140 |
141 |
142 |

Indexing

143 |
144 |

Create an index for faster similarity search:

145 | {` 146 | -- For cosine similarity (recommended for OpenAI embeddings) 147 | CREATE INDEX ON items USING hnsw (embedding vector_cosine_ops); 148 | 149 | -- For L2 distance 150 | CREATE INDEX ON items USING hnsw (embedding vector_l2_ops); 151 | 152 | -- For inner product 153 | CREATE INDEX ON items USING hnsw (embedding vector_ip_ops); 154 | `} 155 |
156 |
157 | 158 |
159 |

Index Options

160 |
161 |

Customize HNSW parameters:

162 | {` 163 | CREATE INDEX ON items 164 | USING hnsw (embedding vector_cosine_ops) 165 | WITH (m = 16, ef_construction = 64); 166 | `} 167 |
168 |
169 |
170 |
171 | 172 | 173 |
174 |

175 | VectorDB Usage 176 |

177 |

178 | Configure and use the VectorDB library for efficient vector 179 | storage and retrieval. 180 |

181 |
182 | 183 |
184 |
185 |

Configuration

186 |
187 | {` 188 | const vectorDB = new VectorDB({ 189 | tableName: 'items', 190 | columns: { 191 | id: 'id', 192 | vector: 'embedding', 193 | content: 'text', 194 | metadata: 'metadata', 195 | createdAt: 'createdAt', 196 | } 197 | }, { 198 | embedding: { 199 | model: 'text-embedding-3-small', 200 | dimensions: 1536, 201 | distance: 'cosine', 202 | }, 203 | chunking: { 204 | method: 'paragraph', 205 | fixedSize: 500, 206 | }, 207 | search: { 208 | defaultLimit: 5, 209 | reranking: false, 210 | } 211 | }); 212 | `} 213 |
214 |
215 | 216 |
217 |

Basic Operations

218 |
219 | {` 220 | // Add text with automatic chunking 221 | await vectorDB.addText(text, { 222 | chunkingMethod: 'paragraph', 223 | metadata: { source: 'docs' } 224 | }); 225 | 226 | // Search similar chunks 227 | const results = await vectorDB.searchSimilar(query, { 228 | limit: 5, 229 | distance: 'cosine', 230 | filter: { source: 'docs' } 231 | }); 232 | `} 233 |
234 |
235 |
236 |
237 | 238 | 239 |
240 |

241 | Example Usage 242 |

243 |

244 | Explore examples of integrating vector search and RAG 245 | capabilities into your Next.js application. 246 |

247 |
248 | 249 |
250 |
251 |

Chat with Documents

252 |
253 |

254 | See the chat implementation in src/app/(chat){" "} 255 | for a complete example of: 256 |

257 |
    258 |
  • Document ingestion with chunking
  • 259 |
  • Semantic search with metadata filtering
  • 260 |
  • Streaming chat responses with context
  • 261 |
262 |
263 |
264 | 265 |
266 |

Chunking Methods

267 |
268 | {` 269 | // Sentence-based chunking 270 | const chunks = vectorDB.chunkText(text, 'sentence'); 271 | 272 | // Paragraph-based chunking 273 | const chunks = vectorDB.chunkText(text, 'paragraph'); 274 | 275 | // Fixed-size chunking 276 | const chunks = vectorDB.chunkText(text, 'fixed'); 277 | `} 278 |
279 |
280 | 281 |
282 |

Similarity Metrics

283 |
284 | {` 285 | // Cosine similarity (normalized vectors) 286 | const results = await vectorDB.searchSimilar(query, { 287 | distance: 'cosine' 288 | }); 289 | 290 | // Euclidean distance 291 | const results = await vectorDB.searchSimilar(query, { 292 | distance: 'euclidean' 293 | }); 294 | 295 | // Inner product 296 | const results = await vectorDB.searchSimilar(query, { 297 | distance: 'inner_product' 298 | }); 299 | `} 300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 | 308 |
309 |

310 | Need help? Check out our{" "} 311 | 315 | GitHub repository 316 | {" "} 317 | or join our community. 318 |

319 |
320 |
321 | ); 322 | } 323 | -------------------------------------------------------------------------------- /src/app/(docs)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Navbar } from "@/components/navbar"; 2 | 3 | export default function DocsLayout({ 4 | children, 5 | }: { 6 | children: React.ReactNode; 7 | }) { 8 | return ( 9 |
10 | 11 |
{children}
12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/app/api/chat/route.ts: -------------------------------------------------------------------------------- 1 | import { openai } from '@ai-sdk/openai'; 2 | import { streamText, StreamData } from 'ai'; 3 | import { searchSimilarChunks } from '@/lib/actions/search'; 4 | import { QueryResultRow } from '@vercel/postgres'; 5 | 6 | export const maxDuration = 30; 7 | 8 | export async function POST(req: Request) { 9 | const { messages } = await req.json(); 10 | const lastMessage = messages[messages.length - 1]; 11 | 12 | // Create StreamData instance 13 | const data = new StreamData(); 14 | 15 | // Get search results 16 | const searchResults = await searchSimilarChunks(lastMessage.content); 17 | 18 | // Format context with metadata 19 | const contextDetails = searchResults?.length 20 | ? searchResults.map((r: QueryResultRow) => ({ 21 | chunk: r.chunk, 22 | metadata: { 23 | distance: r.distance.toFixed(3), 24 | createdAt: new Date(r.createdAt).toLocaleDateString(), 25 | ...r.metadata, 26 | }, 27 | })) 28 | : []; 29 | 30 | // Append context to stream data 31 | data.append({ contextDetails }); 32 | 33 | const context = contextDetails.length 34 | ? `Relevant context:\n${contextDetails 35 | .map( 36 | (r) => 37 | `${r.chunk}\n(Distance: ${r.metadata.distance}, Created: ${r.metadata.createdAt}, Method: ${r.metadata.chunkingMethod}, Index: ${r.metadata.chunkIndex}/${r.metadata.totalChunks})` 38 | ) 39 | .join('\n\n')}\n\n` 40 | : ''; 41 | 42 | const result = await streamText({ 43 | model: openai('gpt-4o-mini'), 44 | messages: [ 45 | { 46 | role: 'system', 47 | content: 48 | 'You are a helpful AI assistant. Use the provided context to answer questions when available. Always start your response with a brief mention of which context you used, if any.', 49 | }, 50 | ...(context 51 | ? [ 52 | { 53 | role: 'system', 54 | content: context, 55 | }, 56 | ] 57 | : []), 58 | ...messages, 59 | ], 60 | onFinish: () => { 61 | // Close the stream when done 62 | data.close(); 63 | }, 64 | }); 65 | 66 | // Return the response with the additional stream data 67 | return result.toDataStreamResponse({ data }); 68 | } 69 | -------------------------------------------------------------------------------- /src/app/api/healthcheck/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { healthCheck } from '@/lib/db/pg'; 3 | 4 | export async function GET() { 5 | console.log('in pg route'); 6 | try { 7 | const isHealthy = await healthCheck(); 8 | if (!isHealthy) { 9 | return NextResponse.json( 10 | { error: 'Database is not healthy' }, 11 | { status: 500 } 12 | ); 13 | } else { 14 | console.log('Database is healthy'); 15 | return NextResponse.json({ message: 'Database is healthy' }); 16 | } 17 | } catch (error: any) { 18 | console.error('Database error details:', { 19 | code: error.code, 20 | message: error.message, 21 | stack: error.stack, 22 | }); 23 | 24 | const errorMessage = 25 | error.code === 'ECONNREFUSED' 26 | ? 'Unable to connect to database - please check if PostgreSQL is running' 27 | : 'Failed to fetch data'; 28 | 29 | return NextResponse.json({ error: errorMessage }, { status: 500 }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/app/api/inngest/events/route.ts: -------------------------------------------------------------------------------- 1 | import { inngest } from '@/inngest/client'; 2 | import { NextResponse } from 'next/server'; 3 | 4 | export async function GET() { 5 | try { 6 | // Fetch recent events from Inngest 7 | const events = await inngest.listEvents({ 8 | limit: 20, 9 | orderBy: 'desc', 10 | }); 11 | 12 | return NextResponse.json({ events }); 13 | } catch (error) { 14 | console.error('Failed to fetch Inngest events:', error); 15 | return NextResponse.json( 16 | { error: 'Failed to fetch events' }, 17 | { status: 500 } 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/app/api/inngest/route.ts: -------------------------------------------------------------------------------- 1 | import { serve } from 'inngest/next'; 2 | import { inngest } from '@/inngest/client'; 3 | import { embedText } from '@/inngest/functions/embedding'; 4 | import { retrieveSimilar } from '@/inngest/functions/retrieval'; 5 | 6 | export const { GET, POST, PUT } = serve({ 7 | client: inngest, 8 | functions: [embedText, retrieveSimilar], 9 | }); 10 | -------------------------------------------------------------------------------- /src/app/api/search/route.ts: -------------------------------------------------------------------------------- 1 | import { searchSimilarChunks } from '@/lib/actions/search'; 2 | import { NextResponse } from 'next/server'; 3 | 4 | export async function POST(req: Request) { 5 | try { 6 | const { query } = await req.json(); 7 | if (!query) { 8 | return NextResponse.json({ error: 'Query is required' }, { status: 400 }); 9 | } 10 | 11 | const results = await searchSimilarChunks(query); 12 | return NextResponse.json({ results }); 13 | } catch (error) { 14 | console.error('Search error:', error); 15 | return NextResponse.json({ error: 'Failed to search' }, { status: 500 }); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HamedMP/NextRag/2c7e85018220e7b001710a28b88d27338d9327e6/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HamedMP/NextRag/2c7e85018220e7b001710a28b88d27338d9327e6/src/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /src/app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HamedMP/NextRag/2c7e85018220e7b001710a28b88d27338d9327e6/src/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 240 10% 3.9%; 9 | --card: 0 0% 100%; 10 | --card-foreground: 240 10% 3.9%; 11 | --popover: 0 0% 100%; 12 | --popover-foreground: 240 10% 3.9%; 13 | --primary: 240 5.9% 10%; 14 | --primary-foreground: 0 0% 98%; 15 | --secondary: 240 4.8% 95.9%; 16 | --secondary-foreground: 240 5.9% 10%; 17 | --muted: 240 4.8% 95.9%; 18 | --muted-foreground: 240 3.8% 46.1%; 19 | --accent: 240 4.8% 95.9%; 20 | --accent-foreground: 240 5.9% 10%; 21 | --destructive: 0 84.2% 60.2%; 22 | --destructive-foreground: 0 0% 98%; 23 | --border: 240 5.9% 90%; 24 | --input: 240 5.9% 90%; 25 | --ring: 240 5.9% 10%; 26 | --radius: 0.5rem; 27 | } 28 | 29 | .dark { 30 | --background: 240 10% 3.9%; 31 | --foreground: 0 0% 98%; 32 | --card: 240 10% 3.9%; 33 | --card-foreground: 0 0% 98%; 34 | --popover: 240 10% 3.9%; 35 | --popover-foreground: 0 0% 98%; 36 | --primary: 0 0% 98%; 37 | --primary-foreground: 240 5.9% 10%; 38 | --secondary: 240 3.7% 15.9%; 39 | --secondary-foreground: 0 0% 98%; 40 | --muted: 240 3.7% 15.9%; 41 | --muted-foreground: 240 5% 64.9%; 42 | --accent: 240 3.7% 15.9%; 43 | --accent-foreground: 0 0% 98%; 44 | --destructive: 0 62.8% 30.6%; 45 | --destructive-foreground: 0 0% 98%; 46 | --border: 240 3.7% 15.9%; 47 | --input: 240 3.7% 15.9%; 48 | --ring: 240 4.9% 83.9%; 49 | } 50 | } 51 | 52 | @layer base { 53 | * { 54 | @apply border-border; 55 | } 56 | body { 57 | @apply bg-background text-foreground; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { GeistMono } from "geist/font/mono"; 2 | import { GeistSans } from "geist/font/sans"; 3 | import type { Metadata } from "next"; 4 | import "./globals.css"; 5 | import { ThemeProvider } from "@/components/theme-provider"; 6 | import { Toaster } from "sonner"; 7 | 8 | export const metadata: Metadata = { 9 | title: "RAG Engineering Demo", 10 | description: "Vector Search & RAG Implementation with Next.js and PostgreSQL", 11 | }; 12 | 13 | export default function RootLayout({ 14 | children, 15 | }: { 16 | children: React.ReactNode; 17 | }) { 18 | return ( 19 | 20 | 23 | 29 | 30 | {children} 31 | 32 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/app/opengraph-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HamedMP/NextRag/2c7e85018220e7b001710a28b88d27338d9327e6/src/app/opengraph-image.png -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { Navbar } from "@/components/navbar"; 2 | import { TechIcons } from "@/components/tech-icons"; 3 | import { Button } from "@/components/ui/button"; 4 | import { Card } from "@/components/ui/card"; 5 | import { DotPattern } from "@/components/ui/dot-pattern"; 6 | import { env } from "@/lib/env.mjs"; 7 | import { cn } from "@/lib/utils"; 8 | import { Braces, Database, GitBranch, Terminal } from "lucide-react"; 9 | import Link from "next/link"; 10 | 11 | export default function Home() { 12 | return ( 13 |
14 | 15 |
16 |
17 |

18 | NextRAG 19 |

20 |

21 | Production-ready implementation of Retrieval Augmented Generation 22 | with vector search capabilities using PostgreSQL + pgvector in 23 | Next.js. 24 |

25 |
26 | 27 | 28 | 29 |
30 | 31 |
32 | 33 |

Core Stack

34 |
    35 |
  • 36 | PostgreSQL + pgvector 37 |
  • 38 |
  • 39 | Inngest pipelines 40 |
  • 41 |
  • 42 | Next.js 15 43 |
  • 44 |
45 |
46 | 49 |
50 | 51 | 52 | 53 |
54 |
55 |
56 | // Try it out 57 |
58 | RAG Playground 59 |
60 |
61 | 72 |
73 | 74 |
75 | 76 | 77 |
78 | 79 | 90 |
91 | ); 92 | } 93 | -------------------------------------------------------------------------------- /src/app/twitter-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HamedMP/NextRag/2c7e85018220e7b001710a28b88d27338d9327e6/src/app/twitter-image.png -------------------------------------------------------------------------------- /src/components/navbar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ThemeToggle } from "@/components/theme-toggle"; 4 | import Link from "next/link"; 5 | import { usePathname } from "next/navigation"; 6 | 7 | const navItems = [ 8 | { href: "/", label: "Home" }, 9 | { href: "/docs", label: "Documentation" }, 10 | { href: "/chat", label: "Playground" }, 11 | ]; 12 | 13 | export function Navbar() { 14 | const pathname = usePathname(); 15 | 16 | return ( 17 |
18 |
19 | 20 | NextRAG 21 | 22 |
23 | 38 | 39 |
40 |
41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/components/tabs/chat-tab.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; 3 | import { ScrollArea } from "@/components/ui/scroll-area"; 4 | import { useChat } from "ai/react"; 5 | import { toast } from "sonner"; 6 | import ReactMarkdown from "react-markdown"; 7 | import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; 8 | import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism"; 9 | import type { SyntaxHighlighterProps } from "react-syntax-highlighter"; 10 | 11 | export function ChatTab() { 12 | const isProduction = process.env.NODE_ENV === "production"; 13 | const { messages, input, handleInputChange, handleSubmit, data } = useChat({ 14 | api: "/api/chat", 15 | }); 16 | 17 | const handleChatSubmit = async (e: React.FormEvent) => { 18 | e.preventDefault(); 19 | if (isProduction) { 20 | toast.error("Chat is disabled in production. Please run locally."); 21 | return; 22 | } 23 | if (!input.trim()) { 24 | toast.error("Please enter a question"); 25 | return; 26 | } 27 | 28 | handleSubmit(e); 29 | }; 30 | 31 | return ( 32 | 33 | 34 | Chat 35 | 36 | 37 | 38 |
39 | {messages.map((m) => ( 40 |
46 |
53 | {m.role === "user" ? ( 54 | m.content 55 | ) : ( 56 | <> 57 | {/* Show context details if available */} 58 | {data?.length > 0 && 59 | data[data.length - 1].contextDetails?.length > 0 && ( 60 |
61 |
62 | Context Used: 63 |
64 | {data[data.length - 1].contextDetails.map( 65 | (context, i) => ( 66 |
67 |
68 | {context.chunk} 69 |
70 |
71 | Distance: {context.metadata.distance} | 72 | Created: {context.metadata.createdAt} | 73 | Method: {context.metadata.chunkingMethod} | 74 | Chunk: {context.metadata.chunkIndex + 1}/ 75 | {context.metadata.totalChunks} 76 |
77 |
78 | ), 79 | )} 80 |
81 | )} 82 | 95 | {String(children).replace(/\n$/, "")} 96 | 97 | ) : ( 98 | 99 | {children} 100 | 101 | ); 102 | }, 103 | }} 104 | className="prose prose-invert max-w-none" 105 | > 106 | {m.content} 107 | 108 | 109 | )} 110 |
111 |
112 | ))} 113 |
114 |
115 | 116 |
117 | 128 | 131 |
132 |
133 |
134 | ); 135 | } 136 | -------------------------------------------------------------------------------- /src/components/tabs/ingest-tab.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; 3 | import { Textarea } from "@/components/ui/textarea"; 4 | import { inngest } from "@/inngest/client"; 5 | import { Loader2 } from "lucide-react"; 6 | import { useState } from "react"; 7 | import { toast } from "sonner"; 8 | 9 | export function IngestTab() { 10 | const [text, setText] = useState(""); 11 | const [loading, setLoading] = useState(false); 12 | 13 | const handleEmbed = async () => { 14 | if (!text) { 15 | toast.error("Please enter some text to embed"); 16 | return; 17 | } 18 | 19 | setLoading(true); 20 | const toastId = toast.loading("Processing your text..."); 21 | 22 | try { 23 | await inngest.send({ 24 | name: "embed/text", 25 | data: { 26 | text, 27 | chunkingMethod: "paragraph", 28 | }, 29 | }); 30 | 31 | toast.success("Text successfully processed and stored!", { id: toastId }); 32 | setText(""); 33 | } catch (error) { 34 | console.error("Failed to process:", error); 35 | toast.error("Failed to process text. Please try again.", { id: toastId }); 36 | } finally { 37 | setLoading(false); 38 | } 39 | }; 40 | 41 | return ( 42 | 43 | 44 | Add Knowledge 45 | 46 | 47 |