diff --git a/app/(dashboard)/document-chat/[id]/page.tsx b/app/(dashboard)/document-chat/[id]/page.tsx
new file mode 100644
index 0000000..dd92b72
--- /dev/null
+++ b/app/(dashboard)/document-chat/[id]/page.tsx
@@ -0,0 +1,48 @@
+import { createServerClient } from "@supabase/ssr"
+import { cookies } from "next/headers"
+import { notFound } from "next/navigation"
+import { DocumentChatUI } from "@/components/dashboard/document-chat-ui"
+
+export default async function DocumentChatDetailPage({
+ params
+}: {
+ params: { id: string }
+}) {
+ const cookieStore = await cookies()
+ const supabase = createServerClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
+ {
+ cookies: {
+ get(name) { return cookieStore.get(name)?.value },
+ },
+ }
+ )
+
+ const { data: { user } } = await supabase.auth.getUser()
+
+ if (!user) {
+ return notFound()
+ }
+
+ const { data: doc, error } = await supabase
+ .from('documents')
+ .select('*')
+ .eq('id', params.id)
+ .eq('user_id', user.id)
+ .single()
+
+ if (error || !doc) {
+ return notFound()
+ }
+
+ return (
+
+
+
+ )
+}
diff --git a/app/(dashboard)/document-chat/page.tsx b/app/(dashboard)/document-chat/page.tsx
new file mode 100644
index 0000000..cf5fc64
--- /dev/null
+++ b/app/(dashboard)/document-chat/page.tsx
@@ -0,0 +1,146 @@
+"use client"
+
+import { useState, useEffect } from "react"
+import { useRouter } from "next/navigation"
+import { createBrowserClient } from "@supabase/ssr"
+import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card"
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+import { Loader2, FileText, Upload, Plus } from "lucide-react"
+
+export default function DocumentChatPage() {
+ const router = useRouter()
+ const [documents, setDocuments] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [uploading, setUploading] = useState(false)
+ const [file, setFile] = useState(null)
+
+ const supabase = createBrowserClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
+ )
+
+ useEffect(() => {
+ fetchDocuments()
+ }, [])
+
+ async function fetchDocuments() {
+ setLoading(true)
+ const { data: { user } } = await supabase.auth.getUser()
+ if (user) {
+ const { data, error } = await supabase
+ .from('documents')
+ .select('*')
+ .eq('user_id', user.id)
+ .order('created_at', { ascending: false })
+
+ if (!error && data) {
+ setDocuments(data)
+ }
+ }
+ setLoading(false)
+ }
+
+ async function handleUpload(e: React.FormEvent) {
+ e.preventDefault()
+ if (!file) return
+
+ setUploading(true)
+ const formData = new FormData()
+ formData.append("file", file)
+
+ try {
+ const res = await fetch("/api/documents/upload", {
+ method: "POST",
+ body: formData,
+ })
+
+ if (res.ok) {
+ const data = await res.json()
+ if (data.documentId) {
+ router.push(`/document-chat/${data.documentId}`)
+ }
+ } else {
+ console.error("Upload failed")
+ }
+ } catch (err) {
+ console.error(err)
+ } finally {
+ setUploading(false)
+ setFile(null)
+ }
+ }
+
+ return (
+
+
+
+
Document Chat
+
+ Upload a PDF and chat with it to extract insights, summaries, and answers.
+
+
+
+
+
+
+ Upload New Document
+ Select a PDF file to analyze.
+
+
+
+
+
+
+
+
Your Documents
+ {loading ? (
+
+
+
+ ) : documents.length === 0 ? (
+
+
+
+ No documents yet
+
+ Upload your first PDF document above to start chatting with it.
+
+
+
+ ) : (
+
+ {documents.map((doc) => (
+ router.push(`/document-chat/${doc.id}`)}>
+
+
+ {doc.title}
+
+
+
+ {new Date(doc.created_at).toLocaleDateString()}
+
+
+
+ ))}
+
+ )}
+
+
+ )
+}
diff --git a/app/api/documents/chat/route.ts b/app/api/documents/chat/route.ts
new file mode 100644
index 0000000..82da7c3
--- /dev/null
+++ b/app/api/documents/chat/route.ts
@@ -0,0 +1,81 @@
+import { NextRequest, NextResponse } from "next/server"
+import { createServerClient } from "@supabase/ssr"
+import { generateEmbedding, generateAIResponse } from "@/lib/ai"
+
+export const maxDuration = 60
+
+export async function POST(req: NextRequest) {
+ try {
+ const { documentId, message } = await req.json()
+
+ if (!documentId || !message) {
+ return NextResponse.json({ error: "Missing documentId or message" }, { status: 400 })
+ }
+
+ const res = NextResponse.next()
+ const supabase = createServerClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
+ {
+ cookies: {
+ get(name) { return req.cookies.get(name)?.value },
+ set(name, value, options) { res.cookies.set(name, value, options) },
+ remove(name, options) { res.cookies.set(name, "", { ...options, maxAge: 0 }) },
+ },
+ }
+ )
+
+ const { data: { user } } = await supabase.auth.getUser()
+ if (!user) {
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
+ }
+
+ // Verify document belongs to user
+ const { data: doc, error: docError } = await supabase
+ .from('documents')
+ .select('id')
+ .eq('id', documentId)
+ .eq('user_id', user.id)
+ .single()
+
+ if (docError || !doc) {
+ return NextResponse.json({ error: "Document not found or unauthorized" }, { status: 404 })
+ }
+
+ // Embed the user's question
+ const queryEmbedding = await generateEmbedding(message)
+
+ // Call match_document_chunks RPC
+ const { data: matchData, error: matchError } = await supabase.rpc('match_document_chunks', {
+ query_embedding: queryEmbedding,
+ match_threshold: 0.5,
+ match_count: 5,
+ doc_id: documentId
+ })
+
+ if (matchError) {
+ console.error("Match error:", matchError)
+ return NextResponse.json({ error: "Failed to search document" }, { status: 500 })
+ }
+
+ const contextText = matchData && matchData.length > 0
+ ? matchData.map((m: any) => m.content).join("\n\n---\n\n")
+ : "No highly relevant context found in this document for the query."
+
+ // Generate AI Response with the retrieved context
+ const aiPrompt = `Based on the following excerpts from a document, answer the user's question.
+If the answer is not in the context, say "I couldn't find the answer in this document." but try to be helpful if possible.
+
+Document Context:
+${contextText}
+
+User's Question: ${message}`
+
+ const aiResponse = await generateAIResponse(aiPrompt, { mode: 'chat' })
+
+ return NextResponse.json({ reply: aiResponse })
+ } catch (err) {
+ console.error("Chat error:", err)
+ return NextResponse.json({ error: "Internal server error" }, { status: 500 })
+ }
+}
diff --git a/app/api/documents/upload/route.ts b/app/api/documents/upload/route.ts
new file mode 100644
index 0000000..92be12a
--- /dev/null
+++ b/app/api/documents/upload/route.ts
@@ -0,0 +1,122 @@
+import { NextRequest, NextResponse } from "next/server"
+import { createServerClient } from "@supabase/ssr"
+import pdfParse from "pdf-parse/lib/pdf-parse"
+import { generateEmbedding } from "@/lib/ai"
+
+export const maxDuration = 60
+
+export async function POST(req: NextRequest) {
+ try {
+ const formData = await req.formData()
+ const file = formData.get("file") as File | null
+ if (!file) {
+ return NextResponse.json({ error: "No file provided" }, { status: 400 })
+ }
+
+ const res = NextResponse.next()
+ const supabase = createServerClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
+ {
+ cookies: {
+ get(name) { return req.cookies.get(name)?.value },
+ set(name, value, options) { res.cookies.set(name, value, options) },
+ remove(name, options) { res.cookies.set(name, "", { ...options, maxAge: 0 }) },
+ },
+ }
+ )
+
+ const { data: { user } } = await supabase.auth.getUser()
+ if (!user) {
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
+ }
+
+ // 1. Upload file to Supabase Storage
+ const fileExt = file.name.split('.').pop()
+ const fileName = `${user.id}/${Date.now()}.${fileExt}`
+
+ // Ensure you have a 'documents' bucket in Supabase!
+ const { data: uploadData, error: uploadError } = await supabase.storage
+ .from('documents')
+ .upload(fileName, file)
+
+ if (uploadError) {
+ console.error("Storage upload error:", uploadError)
+ return NextResponse.json({ error: "Failed to upload file to storage" }, { status: 500 })
+ }
+
+ const file_url = supabase.storage.from('documents').getPublicUrl(fileName).data.publicUrl
+
+ // 2. Parse PDF Text
+ const arrayBuffer = await file.arrayBuffer()
+ const buffer = Buffer.from(arrayBuffer)
+ const pdfData = await pdfParse(buffer)
+ const text = pdfData.text
+
+ // 3. Create document record in DB
+ const { data: doc, error: docError } = await supabase
+ .from('documents')
+ .insert({
+ user_id: user.id,
+ title: file.name,
+ file_url: file_url
+ })
+ .select('id')
+ .single()
+
+ if (docError || !doc) {
+ console.error("Database insert error:", docError)
+ return NextResponse.json({ error: "Failed to create document record" }, { status: 500 })
+ }
+
+ // 4. Chunk text and generate embeddings
+ // Simple chunking by paragraphs or length
+ const chunkSize = 1000
+ const chunks: string[] = []
+ let currentChunk = ""
+
+ const paragraphs = text.split('\n\n')
+ for (const p of paragraphs) {
+ if (currentChunk.length + p.length > chunkSize && currentChunk.length > 0) {
+ chunks.push(currentChunk)
+ currentChunk = ""
+ }
+ currentChunk += p + "\n\n"
+ }
+ if (currentChunk.trim().length > 0) {
+ chunks.push(currentChunk)
+ }
+
+ // 5. Insert chunks into DB
+ const chunkPromises = chunks.filter(c => c.trim().length > 0).map(async (chunkContent) => {
+ try {
+ const embedding = await generateEmbedding(chunkContent)
+ return {
+ document_id: doc.id,
+ content: chunkContent,
+ embedding
+ }
+ } catch (err) {
+ console.error("Failed to generate embedding for chunk:", err)
+ return null
+ }
+ })
+
+ const resolvedChunks = (await Promise.all(chunkPromises)).filter(c => c !== null)
+
+ if (resolvedChunks.length > 0) {
+ const { error: chunkError } = await supabase
+ .from('document_chunks')
+ .insert(resolvedChunks)
+
+ if (chunkError) {
+ console.error("Failed to insert chunks:", chunkError)
+ }
+ }
+
+ return NextResponse.json({ success: true, documentId: doc.id, fileUrl: file_url })
+ } catch (err) {
+ console.error("Upload error:", err)
+ return NextResponse.json({ error: "Internal server error" }, { status: 500 })
+ }
+}
diff --git a/components/dashboard/document-chat-ui.tsx b/components/dashboard/document-chat-ui.tsx
new file mode 100644
index 0000000..d08fd57
--- /dev/null
+++ b/components/dashboard/document-chat-ui.tsx
@@ -0,0 +1,154 @@
+"use client"
+
+import { useState, useRef, useEffect } from "react"
+import { Send, User, Bot, Loader2, ArrowLeft } from "lucide-react"
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+import { ScrollArea } from "@/components/ui/scroll-area"
+import { Avatar, AvatarFallback } from "@/components/ui/avatar"
+import Link from "next/link"
+
+interface DocumentChatUIProps {
+ documentId: string
+ fileUrl: string
+ title: string
+}
+
+interface Message {
+ id: string
+ role: "user" | "ai"
+ content: string
+}
+
+export function DocumentChatUI({ documentId, fileUrl, title }: DocumentChatUIProps) {
+ const [messages, setMessages] = useState([
+ { id: "1", role: "ai", content: `Hello! I've analyzed "${title}". What would you like to know?` }
+ ])
+ const [input, setInput] = useState("")
+ const [loading, setLoading] = useState(false)
+ const scrollRef = useRef(null)
+
+ useEffect(() => {
+ if (scrollRef.current) {
+ scrollRef.current.scrollIntoView({ behavior: "smooth" })
+ }
+ }, [messages])
+
+ async function handleSend(e: React.FormEvent) {
+ e.preventDefault()
+ if (!input.trim() || loading) return
+
+ const userMessage = input.trim()
+ setInput("")
+
+ setMessages(prev => [...prev, { id: Date.now().toString(), role: "user", content: userMessage }])
+ setLoading(true)
+
+ try {
+ const res = await fetch("/api/documents/chat", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ documentId, message: userMessage }),
+ })
+
+ if (res.ok) {
+ const data = await res.json()
+ setMessages(prev => [...prev, { id: (Date.now() + 1).toString(), role: "ai", content: data.reply }])
+ } else {
+ setMessages(prev => [...prev, { id: (Date.now() + 1).toString(), role: "ai", content: "Sorry, I encountered an error searching the document." }])
+ }
+ } catch (err) {
+ console.error(err)
+ setMessages(prev => [...prev, { id: (Date.now() + 1).toString(), role: "ai", content: "Connection error." }])
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ return (
+
+ {/* Left side: PDF Viewer */}
+
+
+
+
+
+
+
+
+
+ {/* Right side: Chat */}
+
+
+
Chat about {title}
+
+
+
+
+ {messages.map((msg) => (
+
+
+ {msg.role === "ai" ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+ {msg.content}
+
+
+ ))}
+ {loading && (
+
+ )}
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/lib/ai.ts b/lib/ai.ts
index f6babb6..9d6f0ce 100644
--- a/lib/ai.ts
+++ b/lib/ai.ts
@@ -265,6 +265,15 @@ async function callGroq(prompt: string): Promise {
return text
}
+export async function generateEmbedding(text: string): Promise {
+ const key = getGeminiKey()
+ const client = new GoogleGenerativeAI(key)
+ const model = client.getGenerativeModel({ model: "text-embedding-004" })
+ const result = await model.embedContent(text)
+ const embedding = result.embedding
+ return embedding.values
+}
+
async function callGemini(prompt: string): Promise {
const key = getGeminiKey()
const client = new GoogleGenerativeAI(key)
diff --git a/package-lock.json b/package-lock.json
index ccbc9ad..13f7904 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -40,6 +40,7 @@
"@radix-ui/react-tooltip": "1.2.8",
"@supabase/ssr": "^0.9.0",
"@supabase/supabase-js": "^2.99.3",
+ "@types/pdf-parse": "^1.1.5",
"@vercel/analytics": "^1.6.1",
"autoprefixer": "^10.4.20",
"class-variance-authority": "^0.7.1",
@@ -56,6 +57,7 @@
"next": "^16.2.0",
"next-themes": "^0.4.6",
"nodemailer": "^8.0.3",
+ "pdf-parse": "^2.4.5",
"razorpay": "^2.9.6",
"react": "18.3.1",
"react-day-picker": "9.13.2",
@@ -1863,6 +1865,190 @@
"node": ">=18"
}
},
+ "node_modules/@napi-rs/canvas": {
+ "version": "0.1.80",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.80.tgz",
+ "integrity": "sha512-DxuT1ClnIPts1kQx8FBmkk4BQDTfI5kIzywAaMjQSXfNnra5UFU9PwurXrl+Je3bJ6BGsp/zmshVVFbCmyI+ww==",
+ "license": "MIT",
+ "workspaces": [
+ "e2e/*"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "optionalDependencies": {
+ "@napi-rs/canvas-android-arm64": "0.1.80",
+ "@napi-rs/canvas-darwin-arm64": "0.1.80",
+ "@napi-rs/canvas-darwin-x64": "0.1.80",
+ "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.80",
+ "@napi-rs/canvas-linux-arm64-gnu": "0.1.80",
+ "@napi-rs/canvas-linux-arm64-musl": "0.1.80",
+ "@napi-rs/canvas-linux-riscv64-gnu": "0.1.80",
+ "@napi-rs/canvas-linux-x64-gnu": "0.1.80",
+ "@napi-rs/canvas-linux-x64-musl": "0.1.80",
+ "@napi-rs/canvas-win32-x64-msvc": "0.1.80"
+ }
+ },
+ "node_modules/@napi-rs/canvas-android-arm64": {
+ "version": "0.1.80",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.80.tgz",
+ "integrity": "sha512-sk7xhN/MoXeuExlggf91pNziBxLPVUqF2CAVnB57KLG/pz7+U5TKG8eXdc3pm0d7Od0WreB6ZKLj37sX9muGOQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/canvas-darwin-arm64": {
+ "version": "0.1.80",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.80.tgz",
+ "integrity": "sha512-O64APRTXRUiAz0P8gErkfEr3lipLJgM6pjATwavZ22ebhjYl/SUbpgM0xcWPQBNMP1n29afAC/Us5PX1vg+JNQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/canvas-darwin-x64": {
+ "version": "0.1.80",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.80.tgz",
+ "integrity": "sha512-FqqSU7qFce0Cp3pwnTjVkKjjOtxMqRe6lmINxpIZYaZNnVI0H5FtsaraZJ36SiTHNjZlUB69/HhxNDT1Aaa9vA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
+ "version": "0.1.80",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.80.tgz",
+ "integrity": "sha512-eyWz0ddBDQc7/JbAtY4OtZ5SpK8tR4JsCYEZjCE3dI8pqoWUC8oMwYSBGCYfsx2w47cQgQCgMVRVTFiiO38hHQ==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/canvas-linux-arm64-gnu": {
+ "version": "0.1.80",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.80.tgz",
+ "integrity": "sha512-qwA63t8A86bnxhuA/GwOkK3jvb+XTQaTiVML0vAWoHyoZYTjNs7BzoOONDgTnNtr8/yHrq64XXzUoLqDzU+Uuw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/canvas-linux-arm64-musl": {
+ "version": "0.1.80",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.80.tgz",
+ "integrity": "sha512-1XbCOz/ymhj24lFaIXtWnwv/6eFHXDrjP0jYkc6iHQ9q8oXKzUX1Lc6bu+wuGiLhGh2GS/2JlfORC5ZcXimRcg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
+ "version": "0.1.80",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.80.tgz",
+ "integrity": "sha512-XTzR125w5ZMs0lJcxRlS1K3P5RaZ9RmUsPtd1uGt+EfDyYMu4c6SEROYsxyatbbu/2+lPe7MPHOO/0a0x7L/gw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/canvas-linux-x64-gnu": {
+ "version": "0.1.80",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.80.tgz",
+ "integrity": "sha512-BeXAmhKg1kX3UCrJsYbdQd3hIMDH/K6HnP/pG2LuITaXhXBiNdh//TVVVVCBbJzVQaV5gK/4ZOCMrQW9mvuTqA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/canvas-linux-x64-musl": {
+ "version": "0.1.80",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.80.tgz",
+ "integrity": "sha512-x0XvZWdHbkgdgucJsRxprX/4o4sEed7qo9rCQA9ugiS9qE2QvP0RIiEugtZhfLH3cyI+jIRFJHV4Fuz+1BHHMg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@napi-rs/canvas-win32-x64-msvc": {
+ "version": "0.1.80",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.80.tgz",
+ "integrity": "sha512-Z8jPsM6df5V8B1HrCHB05+bDiCxjE9QA//3YrkKIdVDEwn5RKaqOxCJDRJkl48cJbylcrJbW4HxZbTte8juuPg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
"node_modules/@next/env": {
"version": "16.2.9",
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.9.tgz",
@@ -4304,6 +4490,15 @@
"integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==",
"license": "MIT"
},
+ "node_modules/@types/pdf-parse": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@types/pdf-parse/-/pdf-parse-1.1.5.tgz",
+ "integrity": "sha512-kBfrSXsloMnUJOKi25s3+hRmkycHfLK6A09eRGqF/N8BkQoPUmaCr+q8Cli5FnfohEz/rsv82zAiPz/LXtOGhA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/phoenix": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.7.tgz",
@@ -4321,7 +4516,6 @@
"version": "19.2.14",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
- "dev": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.2.2"
@@ -4331,7 +4525,7 @@
"version": "19.2.3",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^19.2.0"
@@ -9270,6 +9464,38 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/pdf-parse": {
+ "version": "2.4.5",
+ "resolved": "https://registry.npmjs.org/pdf-parse/-/pdf-parse-2.4.5.tgz",
+ "integrity": "sha512-mHU89HGh7v+4u2ubfnevJ03lmPgQ5WU4CxAVmTSh/sxVTEDYd1er/dKS/A6vg77NX47KTEoihq8jZBLr8Cxuwg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@napi-rs/canvas": "0.1.80",
+ "pdfjs-dist": "5.4.296"
+ },
+ "bin": {
+ "pdf-parse": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": ">=20.16.0 <21 || >=22.3.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/mehmet-kozan"
+ }
+ },
+ "node_modules/pdfjs-dist": {
+ "version": "5.4.296",
+ "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.296.tgz",
+ "integrity": "sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=20.16.0 || >=22.3.0"
+ },
+ "optionalDependencies": {
+ "@napi-rs/canvas": "^0.1.80"
+ }
+ },
"node_modules/performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
diff --git a/package.json b/package.json
index 10d0e1a..956ccc7 100644
--- a/package.json
+++ b/package.json
@@ -43,6 +43,7 @@
"@radix-ui/react-tooltip": "1.2.8",
"@supabase/ssr": "^0.9.0",
"@supabase/supabase-js": "^2.99.3",
+ "@types/pdf-parse": "^1.1.5",
"@vercel/analytics": "^1.6.1",
"autoprefixer": "^10.4.20",
"class-variance-authority": "^0.7.1",
@@ -59,6 +60,7 @@
"next": "^16.2.0",
"next-themes": "^0.4.6",
"nodemailer": "^8.0.3",
+ "pdf-parse": "^2.4.5",
"razorpay": "^2.9.6",
"react": "18.3.1",
"react-day-picker": "9.13.2",
diff --git a/supabase/migration-document-chat.sql b/supabase/migration-document-chat.sql
new file mode 100644
index 0000000..447b359
--- /dev/null
+++ b/supabase/migration-document-chat.sql
@@ -0,0 +1,85 @@
+-- Enable the pgvector extension to work with embedding vectors
+create extension if not exists vector;
+
+-- Create a table to store documents
+create table if not exists documents (
+ id uuid primary key default gen_random_uuid(),
+ user_id uuid references auth.users(id) on delete cascade not null,
+ title text not null,
+ file_url text not null, -- The path in Supabase Storage or external URL
+ created_at timestamp with time zone default timezone('utc'::text, now()) not null
+);
+
+-- Enable RLS for documents
+alter table documents enable row level security;
+
+create policy "Users can view their own documents"
+ on documents for select
+ using (auth.uid() = user_id);
+
+create policy "Users can insert their own documents"
+ on documents for insert
+ with check (auth.uid() = user_id);
+
+create policy "Users can delete their own documents"
+ on documents for delete
+ using (auth.uid() = user_id);
+
+-- Create a table to store the text chunks and their embeddings
+create table if not exists document_chunks (
+ id uuid primary key default gen_random_uuid(),
+ document_id uuid references documents(id) on delete cascade not null,
+ content text not null,
+ embedding vector(768) not null -- 768 is the dimension for Gemini text-embedding-004
+);
+
+-- Enable RLS for document_chunks (policies usually follow the parent document)
+alter table document_chunks enable row level security;
+
+create policy "Users can view chunks of their documents"
+ on document_chunks for select
+ using (
+ exists (
+ select 1 from documents
+ where documents.id = document_chunks.document_id
+ and documents.user_id = auth.uid()
+ )
+ );
+
+create policy "Users can insert chunks into their documents"
+ on document_chunks for insert
+ with check (
+ exists (
+ select 1 from documents
+ where documents.id = document_chunks.document_id
+ and documents.user_id = auth.uid()
+ )
+ );
+
+-- Create an index for faster similarity searches
+create index on document_chunks using hnsw (embedding vector_cosine_ops);
+
+-- Create a function to similarity search for document chunks
+create or replace function match_document_chunks (
+ query_embedding vector(768),
+ match_threshold float,
+ match_count int,
+ doc_id uuid
+)
+returns table (
+ id uuid,
+ content text,
+ similarity float
+)
+language sql stable
+as $$
+ select
+ document_chunks.id,
+ document_chunks.content,
+ 1 - (document_chunks.embedding <=> query_embedding) as similarity
+ from document_chunks
+ where document_chunks.document_id = doc_id
+ and 1 - (document_chunks.embedding <=> query_embedding) > match_threshold
+ order by document_chunks.embedding <=> query_embedding
+ limit match_count;
+$$;