diff --git a/.agent/tasks/task.md b/.agent/tasks/task.md
new file mode 100644
index 0000000..043133f
--- /dev/null
+++ b/.agent/tasks/task.md
@@ -0,0 +1,10 @@
+# Task: Re-apply Model Fixes
+
+## Observations
+1. **Gemini Error**: `models/gemini-1.5-pro` 404s. Using `gemini-1.5-pro-latest` previously resolved this (passing the error to the next provider).
+2. **OpenAI Error**: `gpt-4-turbo` error ("model does not exist"). Only appeared after Gemini was fixed.
+
+## Plan
+- [ ] Update `app/api/chat/provider.ts` again to use correct model identifiers.
+ - Gemini: `gemini-1.5-flash-latest`, `gemini-1.5-pro-latest`
+ - OpenAI: `gpt-4o` (widely available and valid)
diff --git a/.env.example b/.env.example
index 2d605af..a404af7 100644
--- a/.env.example
+++ b/.env.example
@@ -25,3 +25,19 @@ UPSTASH_REDIS_REST_TOKEN=xxxxxxxxx
# # Upstash
# UPSTASH_REDIS_REST_URL=xxxxxxxxx
# UPSTASH_REDIS_REST_TOKEN=xxxxxxxxx
+
+# AI Gateway (routing / unified LLM access)
+AI_GATEWAY_API_KEY=xxxxxxxxx
+
+# Google Gemini / Generative AI
+GEMINI_API_KEY=xxxxxxxxx
+GOOGLE_GENERATIVE_AI_API_KEY=xxxxxxxxx
+
+# OpenAI (GPT models)
+OPENAI_API_KEY=xxxxxxxxx
+
+# Anthropic (Claude models)
+ANTHROPIC_API_KEY=xxxxxxxxx
+
+# Tavily (Search / Web RAG)
+TAVILY_API_KEY=xxxxxxxxx
diff --git a/README.md b/README.md
index c89505d..3112fb5 100644
--- a/README.md
+++ b/README.md
@@ -13,6 +13,7 @@ A modern SaaS template built with Next.js 15, Tailwind CSS v4, Shadcn UI v2, Ups
- đ§Š **React Hook Form** - Flexible form validation
- âī¸ **Zod** - Schema validation
- đĄī¸ **Enhanced Security** - Robust authentication with rate limiting using Upstash
+- đ **RAG (Chat with Data)** - Upload and chat with PDF, DOCX, TXT, and Images using Google Gemini embeddings
- đ **Security Headers** - CSP and other security headers (Coming Soon)
- đĢ **Anti-Brute Force** - Protection against authentication attacks (Coming Soon)
@@ -62,6 +63,7 @@ supabase start
```
> After the contianer starts, you will be provided with some credentials like the following example:
+>
> ```
> API URL: http://127.0.0.1:54321
> GraphQL URL: http://127.0.0.1:54321/graphql/v1
@@ -76,8 +78,18 @@ supabase start
> S3 Secret Key: 850181e4652dd023b7a98c58ae0d2d34bd487ee0cc3254aed6eda37307425907
> S3 Region: local
> ```
+>
> **Copy those credentials from you terminal** and save them in your notes or create a `supabase-local-credentials.txt` file in this repo (it is already added to `.gitignore` so that it is not pushed into the repository.)
+#### Run RAG Migrations
+
+To enable vector search, you need to apply the RAG database schema:
+
+1. Go to your Supabase Dashboard (http://127.0.0.1:54323).
+2. Open the **SQL Editor**.
+3. Open the file `supabase/migrations/20251217_add_rag_columns.sql` from your project.
+4. Copy the content and run it in the SQL Editor.
+
### 4. Set up environment variables
1. Copy the `.env.example` file to `.env.local`:
@@ -99,6 +111,11 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=xxxxxxxxx
# Upstash
UPSTASH_REDIS_REST_URL=xxxxxxxxx
UPSTASH_REDIS_REST_TOKEN=xxxxxxxxx
+
+# RAG & AI (Google Gemini is required for RAG)
+GOOGLE_GENERATIVE_AI_API_KEY=xxxxxxxxx
+TAVILY_API_KEY=xxxxxxxxx # Optional, for web search
+# OPENAI_API_KEY=xxxxxxxxx # Optional, but Google is preferred for this RAG implementation
```
### 5. Run the development server
@@ -114,7 +131,48 @@ yarn dev
Your application should now be running at [http://localhost:3000](http://localhost:3000).
+### 6. How to use RAG (Chat with Data)
+
+1. **Upload Documents**: Click the paperclip icon in the chat input area.
+2. **Select Files**: Choose PDF, Word (.docx), Text (.txt), or Image files.
+3. **Chat**: Once uploaded, ask questions about your documents. The AI will retrieve relevant context to answer.
+
+## RAG Architecture
+
+### Document Ingestion Flow
+
+```mermaid
+graph TD
+ A["User Uploads File"] --> B["API: /api/process-document"]
+ B --> C{"File Type?"}
+ C -- "PDF/DOCX/TXT" --> D["Extract Text"]
+ C -- "Image" --> E["Gemini Vision Analysis"]
+ D --> F["Chunk Text"]
+ E --> F
+ F --> G["Generate Embeddings (Google/OpenAI)"]
+ G --> H["Store in Supabase (pgvector)"]
+```
+
+### Chat Retrieval Flow
+
+```mermaid
+sequenceDiagram
+ participant User
+ participant API as Chat API
+ participant DB as Supabase (Vector DB)
+ participant LLM as Google Gemini
+
+ User->>API: Send Message
+ API->>API: Generate Embedding for Query
+ API->>DB: match_documents(embedding)
+ DB-->>API: Return Relevant Chunks
+ API->>API: Inject Context into System Prompt
+ API->>LLM: Generate Response (Query + Context)
+ LLM-->>User: Stream Response
+```
+
## Some **Features**
+
- Email/password authentication
- Google OAuth integration
- Strong password requirements
@@ -134,6 +192,10 @@ Your application should now be running at [http://localhost:3000](http://localho
â â âââ reset-password/
â âââ (public)/ # Public routes
â âââ (authenticated)/ # Protected routes
+â â âââ chat/ # Chat interface
+â âââ api/ # API Routes
+â â âââ chat/ # Chat API
+â â âââ process-document/# RAG Ingestion API
â âââ actions/ # Server actions
â âââ globals.css # Global styles
âââ assets/ # Project assets
@@ -141,6 +203,7 @@ Your application should now be running at [http://localhost:3000](http://localho
â âââ logos/ # Logo files
âââ components/ # React components
â âââ ui/ # Shadcn UI components
+â âââ chat/ # Chat components
â âââ mode-toggle.tsx # Dark/light mode toggle
â âââ theme-provider.tsx # Theme context provider
âââ hooks/ # Custom React hooks
diff --git a/app/(authenticated)/chat/[[...sessionId]]/layout.tsx b/app/(authenticated)/chat/[[...sessionId]]/layout.tsx
new file mode 100644
index 0000000..a417c25
--- /dev/null
+++ b/app/(authenticated)/chat/[[...sessionId]]/layout.tsx
@@ -0,0 +1,17 @@
+"use client";
+
+import ChatSidebar from "@/components/chat/ChatSidebar";
+
+export default function ChatLayout({ children }: { children: React.ReactNode }) {
+ return (
+
+
+
+
+
+
+ {children}
+
+
+ );
+}
diff --git a/app/(authenticated)/chat/[[...sessionId]]/page.tsx b/app/(authenticated)/chat/[[...sessionId]]/page.tsx
new file mode 100644
index 0000000..e54b2d2
--- /dev/null
+++ b/app/(authenticated)/chat/[[...sessionId]]/page.tsx
@@ -0,0 +1,365 @@
+'use client';
+import {
+ Conversation,
+ ConversationContent,
+ ConversationScrollButton,
+} from '@/components/ai-elements/conversation';
+import {
+ Message,
+ MessageContent,
+ MessageResponse,
+ MessageActions,
+ MessageAction,
+} from '@/components/ai-elements/message';
+import {
+ PromptInput,
+ PromptInputActionAddAttachments,
+ PromptInputActionMenu,
+ PromptInputActionMenuContent,
+ PromptInputActionMenuTrigger,
+ PromptInputAttachment,
+ PromptInputAttachments,
+ PromptInputBody,
+ PromptInputButton,
+ PromptInputHeader,
+ PromptInputSelect,
+ PromptInputSelectContent,
+ PromptInputSelectItem,
+ PromptInputSelectTrigger,
+ PromptInputSelectValue,
+ PromptInputSubmit,
+ PromptInputTextarea,
+ PromptInputFooter,
+ PromptInputTools,
+} from '@/components/ai-elements/prompt-input';
+import { useState, useEffect } from 'react';
+import { useChat } from '@ai-sdk/react';
+import { CopyIcon, GlobeIcon, RefreshCcwIcon } from 'lucide-react';
+import { toast } from 'sonner';
+import {
+ Source,
+ Sources,
+ SourcesContent,
+ SourcesTrigger,
+} from '@/components/ai-elements/sources';
+import {
+ Reasoning,
+ ReasoningContent,
+ ReasoningTrigger,
+} from '@/components/ai-elements/reasoning';
+import { Loader } from '@/components/ai-elements/loader';
+import { useParams, useRouter } from 'next/navigation';
+import { createClient } from '@/utils/supabase/client';
+
+const models = [
+ { name: 'Gemini (flash)', value: 'gemini' },
+ { name: 'Gemini (lite)', value: 'gemini_flash_lite' },
+ { name: 'Gemini (pro)', value: 'gemini_pro' },
+ { name: 'GPT-4 Turbo', value: 'openai' },
+ { name: 'Claude 3.5', value: 'claude' },
+];
+
+const ChatBotDemo = () => {
+ const params = useParams();
+ const router = useRouter();
+ const sessionId = params.sessionId?.[0]; // Get first item from catch-all route
+
+ const [model, setModel] = useState(models[0].value);
+ const [input, setInput] = useState('');
+ const [webSearch, setWebSearch] = useState(false);
+ const [isLoadingMessages, setIsLoadingMessages] = useState(!!sessionId);
+
+ // Initialize useChat - attachments are handled automatically by PromptInput components
+ const { messages, setMessages, sendMessage, status, regenerate } = useChat({});
+
+ // Load existing messages for conversation context
+ useEffect(() => {
+ if (!sessionId) {
+ setIsLoadingMessages(false);
+ return;
+ }
+
+ const loadMessages = async () => {
+ try {
+ const response = await fetch(`/api/messages?sessionId=${sessionId}`);
+ if (response.ok) {
+ const data = await response.json();
+ // Convert database messages to AI SDK format with parts array
+ const formattedMessages = data.map((msg: any) => ({
+ id: msg.id,
+ role: msg.role,
+ parts: [
+ {
+ type: 'text',
+ text: msg.content,
+ }
+ ],
+ }));
+ // setMessages includes these in conversation context for AI
+ setMessages(formattedMessages);
+ }
+ } catch (error) {
+ console.error('Error loading messages:', error);
+ } finally {
+ setIsLoadingMessages(false);
+ }
+ };
+
+ loadMessages();
+ }, [sessionId, setMessages]);
+
+ const handleSubmit = async (message?: { text?: string; files?: any[] }, e?: React.FormEvent) => {
+ e?.preventDefault();
+ const text = message?.text ?? input;
+ const files = message?.files ?? [];
+
+ if (!text?.trim() && files.length === 0) return;
+
+ // Process files for RAG before sending message
+ if (files.length > 0) {
+ const supabase = createClient();
+ const { data: { user } } = await supabase.auth.getUser();
+
+ if (user) {
+ // Process each file for RAG in the background
+ files.forEach(async (file) => {
+ try {
+ // Upload to Supabase Storage
+ // Use 'filename' and 'mediaType' from PromptInput's FileUIPart, fallback to 'name' and 'type' if standard File
+ const fileName = file.filename || file.name || `file-${Date.now()}`;
+ const fileType = file.mediaType || file.type || 'application/octet-stream';
+
+ const fileId = `${Date.now()}-${fileName}`;
+ const filePath = `${user.id}/${fileId}`;
+
+ // Helper to convert data URL or blob URL to Blob for upload
+ let blobToUpload = file;
+ if (!(file instanceof File) && !(file instanceof Blob)) {
+ // If it's a FileUIPart, we might need to fetch the blob from the URL
+ if (file.url) {
+ const blobRes = await fetch(file.url);
+ blobToUpload = await blobRes.blob();
+ }
+ }
+
+ const { error: uploadError } = await supabase.storage
+ .from('documents')
+ .upload(filePath, blobToUpload, {
+ contentType: fileType,
+ });
+
+ if (uploadError) {
+ console.error('Error uploading file for RAG:', uploadError);
+ toast.error(`Upload failed: ${uploadError.message}`);
+ return;
+ }
+
+ // Process document for RAG
+ // Remove debug payload log
+ const response = await fetch('/api/process-document', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ filePath,
+ fileName: fileName,
+ fileType: fileType,
+ }),
+ });
+
+ const result = await response.json();
+ if (response.ok) {
+ console.log(`â RAG: Processed ${fileName} - ${result.chunksProcessed} chunks created`);
+ toast.success(`${fileName} added to your knowledge base!`);
+ } else {
+ console.error('Error processing document:', result.error);
+ toast.error(`Failed to process ${fileName}: ${result.error}`);
+ }
+ } catch (error) {
+ console.error('Error in RAG processing:', error);
+ }
+ });
+ }
+ }
+
+ // Filter files: Only send images to LLM (for multimodal vision)
+ // Other files (PDFs, Word docs, etc.) only go to RAG, not to LLM
+ const imageFiles = files.filter((file: File) => file.type.startsWith('image/'));
+
+ await sendMessage(
+ {
+ text: text || '',
+ files: imageFiles, // Only images for multimodal vision, others are RAG-only
+ },
+ {
+ body: {
+ modelKey: model,
+ webSearch,
+ sessionId,
+ },
+ }
+ );
+ setInput('');
+ };
+
+ function handleInputChange(event: React.ChangeEvent): void {
+ setInput(event.target.value);
+ }
+
+ return (
+