production-rag / ui /src /components /NotebookTab.jsx
vgsampathkumar's picture
feat: add JWT auth, per-user document isolation, and delete endpoint
0e56eac
Raw
History Blame Contribute Delete
3.78 kB
import { useState, useEffect, useCallback } from 'react'
import { motion } from 'framer-motion'
import { useAuth } from '@clerk/clerk-react'
import NotebookGuide from './NotebookGuide.jsx'
import SuggestedQuestions from './SuggestedQuestions.jsx'
import DocumentSummaryCard from './DocumentSummaryCard.jsx'
import { API_BASE } from '../config.js'
export default function NotebookTab({ onAsk }) {
const { getToken } = useAuth()
const [notebookData, setNotebookData] = useState({ guide: null, summaries: {} })
const [documents, setDocuments] = useState([])
const [loading, setLoading] = useState(true)
const fetchData = useCallback(async () => {
setLoading(true)
try {
const token = await getToken()
const headers = token ? { Authorization: `Bearer ${token}` } : {}
const [nbRes, docsRes] = await Promise.all([
fetch(`${API_BASE}/notebook`, { headers }),
fetch(`${API_BASE}/documents`, { headers }),
])
const nb = await nbRes.json()
const docs = await docsRes.json()
setNotebookData(nb)
setDocuments(docs.documents || [])
} catch {
// backend not reachable
} finally {
setLoading(false)
}
}, [getToken])
useEffect(() => {
fetchData()
}, [fetchData])
const handleGuideReady = (guide) => setNotebookData(prev => ({ ...prev, guide }))
const handleSummaryReady = (source, entry) =>
setNotebookData(prev => ({ ...prev, summaries: { ...prev.summaries, [source]: entry } }))
if (loading) {
return (
<div className="flex items-center justify-center py-20 text-muted">
<motion.svg
width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"
animate={{ rotate: 360 }} transition={{ duration: 1, repeat: Infinity, ease: 'linear' }}
>
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
</motion.svg>
<span className="ml-3 text-sm">Loading notebook…</span>
</div>
)
}
return (
<motion.div
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.35 }}
className="flex flex-col gap-6"
>
<NotebookGuide initialGuide={notebookData.guide} onGuideReady={handleGuideReady} />
{notebookData.guide?.suggested_questions?.length > 0 && (
<SuggestedQuestions questions={notebookData.guide.suggested_questions} onAsk={onAsk} />
)}
{documents.length > 0 && (
<div>
<div className="flex items-center gap-2 mb-3">
<svg className="text-muted" width="13" height="13" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" />
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z" />
</svg>
<h3 className="text-xs font-semibold text-body/70 uppercase tracking-wider">Document Library</h3>
<span className="text-xs text-muted bg-surface px-2 py-0.5 rounded-full border border-white/5">{documents.length}</span>
</div>
<div className="flex flex-col gap-2">
{documents.map(doc => (
<DocumentSummaryCard
key={doc.name}
doc={doc}
cachedSummary={notebookData.summaries?.[doc.name] || null}
onSummaryReady={handleSummaryReady}
/>
))}
</div>
</div>
)}
{documents.length === 0 && (
<div className="text-center py-12 text-muted">
<p className="text-sm">No documents indexed yet.</p>
<p className="text-xs mt-1 opacity-60">Switch to the Chat tab and upload PDFs first.</p>
</div>
)}
</motion.div>
)
}