Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions app/(dashboard)/document-chat/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="p-4 md:p-6 w-full max-w-[1600px] mx-auto">
<DocumentChatUI
documentId={doc.id}
fileUrl={doc.file_url}
title={doc.title}
/>
</div>
)
}
146 changes: 146 additions & 0 deletions app/(dashboard)/document-chat/page.tsx
Original file line number Diff line number Diff line change
@@ -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<any[]>([])
const [loading, setLoading] = useState(true)
const [uploading, setUploading] = useState(false)
const [file, setFile] = useState<File | null>(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 (
<div className="container mx-auto py-8 max-w-5xl space-y-8">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h1 className="text-3xl font-bold tracking-tight">Document Chat</h1>
<p className="text-muted-foreground mt-2">
Upload a PDF and chat with it to extract insights, summaries, and answers.
</p>
</div>
</div>

<Card>
<CardHeader>
<CardTitle>Upload New Document</CardTitle>
<CardDescription>Select a PDF file to analyze.</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleUpload} className="flex gap-4 items-center">
<Input
type="file"
accept=".pdf"
onChange={(e) => setFile(e.target.files?.[0] || null)}
className="max-w-sm"
disabled={uploading}
/>
<Button type="submit" disabled={!file || uploading}>
{uploading ? (
<><Loader2 className="mr-2 h-4 w-4 animate-spin" /> Uploading...</>
) : (
<><Upload className="mr-2 h-4 w-4" /> Upload</>
)}
</Button>
</form>
</CardContent>
</Card>

<div className="space-y-4">
<h2 className="text-xl font-semibold">Your Documents</h2>
{loading ? (
<div className="flex justify-center p-12">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
) : documents.length === 0 ? (
<Card className="bg-muted/50 border-dashed">
<CardContent className="flex flex-col items-center justify-center py-12 text-center">
<FileText className="h-12 w-12 text-muted-foreground mb-4 opacity-50" />
<h3 className="font-semibold text-lg">No documents yet</h3>
<p className="text-muted-foreground text-sm mt-1 max-w-sm">
Upload your first PDF document above to start chatting with it.
</p>
</CardContent>
</Card>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{documents.map((doc) => (
<Card key={doc.id} className="hover:bg-muted/50 transition-colors cursor-pointer group" onClick={() => router.push(`/document-chat/${doc.id}`)}>
<CardHeader>
<CardTitle className="text-base font-medium flex items-start justify-between gap-2">
<span className="truncate">{doc.title}</span>
<FileText className="h-4 w-4 text-primary shrink-0 opacity-50 group-hover:opacity-100 transition-opacity" />
</CardTitle>
<CardDescription className="text-xs">
{new Date(doc.created_at).toLocaleDateString()}
</CardDescription>
</CardHeader>
</Card>
))}
</div>
)}
</div>
</div>
)
}
81 changes: 81 additions & 0 deletions app/api/documents/chat/route.ts
Original file line number Diff line number Diff line change
@@ -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 })
}
}
Loading