"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import Link from "next/link"; import { ArrowRight, CheckCircle2, Clock3, FileText, Flame, LibraryBig, RefreshCw, Search, Trash2, UploadCloud, XCircle, } from "lucide-react"; import { deleteDocument, listDocuments } from "@/lib/api/documents"; import type { ApiDocument, DocumentStatus, PreviousPaper } from "@/lib/api/types"; import { buildDemoSeedDocuments } from "@/lib/demo-seed-documents"; import { getLocalPreviousPapers } from "@/lib/frontend-local-store"; import { buildStudyIntentUrl } from "@/lib/study-intents"; import { cn } from "@/lib/utils"; import { getPublicDemoMode } from "@/lib/env"; import { hasLiveSession } from "@/lib/api/mode"; import { MaterialDeletePrompt } from "@/components/study-chat/MaterialDeletePrompt"; type SourcesSurface = "light" | "dark"; type HeadingLevel = "h1" | "h2"; type SourceFilter = "all" | "notes" | "chapter" | "question_paper"; const FILTERS: Array<{ value: SourceFilter; label: string }> = [ { value: "all", label: "All" }, { value: "notes", label: "Notes" }, { value: "chapter", label: "Chapters" }, { value: "question_paper", label: "Question papers" }, ]; type SourcePalette = { root: string; heading: string; description: string; primaryButton: string; secondaryButton: string; toolbar: string; input: string; inputPlaceholder: string; icon: string; filterActive: string; filterInactive: string; count: string; subtleLink: string; card: string; cardTitle: string; cardMeta: string; divider: string; action: string; actionMuted: string; empty: string; emptyIcon: string; emptyTitle: string; emptyText: string; paperPanel: string; paperHeading: string; paperText: string; paperItem: string; infoPanel: string; infoText: string; skeleton: string; status: { ready: string; processing: string; failed: string; default: string; }; }; function getPalette(surface: SourcesSurface): SourcePalette { if (surface === "light") { return { root: "text-[#15161b]", heading: "text-[#15161b]", description: "text-[#737580]", primaryButton: "border border-[#17181b] bg-[#17181b] text-white hover:bg-[#303139] focus-visible:ring-[#17181b]/30", secondaryButton: "border border-[#dfe1e7] bg-white text-[#34363f] hover:border-[#c8cbd4] hover:bg-[#fafafa] focus-visible:ring-[#17181b]/20", toolbar: "border-[#dfe1e7] bg-white", input: "text-[#202128]", inputPlaceholder: "placeholder:text-[#989aa4]", icon: "text-[#747680]", filterActive: "border-[#b9bbc3] bg-[#17181b] text-white", filterInactive: "border-[#dfe1e7] bg-white text-[#666873] hover:border-[#bfc2cb] hover:text-[#25262c]", count: "text-[#737580]", subtleLink: "text-[#5b5d66] hover:text-[#17181b]", card: "border-[#e1e3e8] bg-white shadow-[0_8px_24px_rgba(20,22,29,0.035)] hover:border-[#c9ccd5] hover:shadow-[0_12px_30px_rgba(20,22,29,0.06)]", cardTitle: "text-[#202128] hover:text-[#17181b]", cardMeta: "text-[#858792]", divider: "border-[#ececf0]", action: "border-[#dfe1e7] bg-white text-[#34363f] hover:border-[#bfc2cb] hover:bg-[#fafafa] hover:text-[#17181b] focus-visible:ring-[#17181b]/20", actionMuted: "border-[#e7e8ed] text-[#62646e] hover:border-[#c9ccd5] hover:text-[#17181b] focus-visible:ring-[#17181b]/20", empty: "border-[#dedfe5] bg-[#fefefd]", emptyIcon: "border-[#e1e3e8] bg-[#f5f6f8] text-[#767883]", emptyTitle: "text-[#202128]", emptyText: "text-[#737580]", paperPanel: "border-[#e1e3e8] bg-white", paperHeading: "text-[#34363f]", paperText: "text-[#737580]", paperItem: "border-[#e5e6eb] bg-[#fafafa] text-[#4d4f58] hover:border-[#c9ccd5] hover:bg-white hover:text-[#17181b] focus-visible:ring-[#17181b]/20", infoPanel: "border-[#e1e3e8] bg-[#fafafa]", infoText: "text-[#737580]", skeleton: "border-[#e7e8ed] bg-white", status: { ready: "border-[#cfe4d7] bg-[#f3faf5] text-[#2f6b49]", processing: "border-[#ead9b8] bg-[#fffaf0] text-[#86621f]", failed: "border-[#ead1d1] bg-[#fff6f6] text-[#9b454d]", default: "border-[#e1e3e8] bg-[#f7f7f8] text-[#656772]", }, }; } return { root: "text-white", heading: "text-white", description: "text-white/52", primaryButton: "border border-white/[0.08] bg-white text-black hover:bg-white/90 focus-visible:ring-white/35", secondaryButton: "border border-white/[0.1] bg-white/[0.02] text-white/70 hover:border-white/20 hover:bg-white/[0.05] hover:text-white focus-visible:ring-white/25", toolbar: "border-white/[0.08] bg-white/[0.02]", input: "text-white", inputPlaceholder: "placeholder:text-white/30", icon: "text-white/40", filterActive: "border-white/20 bg-white/[0.07] text-white", filterInactive: "border-white/[0.08] bg-transparent text-white/50 hover:border-white/15 hover:text-white/80", count: "text-white/50", subtleLink: "text-white/50 hover:text-white", card: "border-white/[0.08] bg-[#0b0b0c] hover:border-white/[0.15]", cardTitle: "text-white hover:text-white", cardMeta: "text-white/50", divider: "border-white/[0.08]", action: "border-white/[0.1] bg-white/[0.02] text-white/70 hover:border-white/20 hover:bg-white/[0.05] hover:text-white focus-visible:ring-white/25", actionMuted: "border-white/[0.08] text-white/55 hover:border-white/15 hover:text-white focus-visible:ring-white/25", empty: "border-white/[0.12] bg-black/30", emptyIcon: "border-white/[0.1] bg-white/[0.035] text-white/40", emptyTitle: "text-white", emptyText: "text-white/52", paperPanel: "border-white/[0.08] bg-white/[0.02]", paperHeading: "text-white/70", paperText: "text-white/60", paperItem: "border-white/[0.06] text-white/70 hover:border-white/20 hover:text-white focus-visible:ring-white/25", infoPanel: "border-white/[0.08] bg-white/[0.02]", infoText: "text-white/50", skeleton: "border-white/[0.06] bg-white/[0.02]", status: { ready: "border-emerald-300/20 bg-emerald-300/[0.08] text-emerald-200/80", processing: "border-amber-300/20 bg-amber-300/[0.08] text-amber-200/80", failed: "border-rose-300/20 bg-rose-300/[0.08] text-rose-200/80", default: "border-white/[0.08] bg-white/[0.05] text-white/50", }, }; } function shouldShowDemoSources(): boolean { const mode = getPublicDemoMode(); if (mode === "demo") return true; if (mode === "live") return false; return !hasLiveSession(); } async function fetchSources() { let docs = await listDocuments(); if (docs.length === 0 && shouldShowDemoSources()) { docs = buildDemoSeedDocuments(); } return { docs, papers: getLocalPreviousPapers() }; } function statusText(status: DocumentStatus) { if (status === "ready") return "Ready to study"; if (status === "processing" || status === "uploaded") return "Reading material"; if (status === "failed") return "Could not read"; return "Saved source"; } function formatDate(iso: string) { const date = new Date(iso); if (Number.isNaN(date.getTime())) return "Date unavailable"; return new Intl.DateTimeFormat("en", { dateStyle: "medium" }).format(date); } function getDocumentMeta(document: ApiDocument) { if (document.chunk_count > 0) { return `${document.chunk_count} ${document.chunk_count === 1 ? "section" : "sections"}`; } const fileName = document.file_name.toLowerCase(); if (document.file_type.includes("pdf") || fileName.endsWith(".pdf")) return "PDF"; if (document.file_type.includes("text") || fileName.endsWith(".txt")) return "TXT"; return "Study material"; } function matchesFilter(document: ApiDocument, filter: SourceFilter) { if (filter === "all") return true; const materialType = (document.material_type ?? "").toLowerCase(); if (filter === "notes") return materialType.includes("note"); return materialType.includes(filter); } function FilterButton({ active, label, onClick, palette, }: { active: boolean; label: string; onClick: () => void; palette: SourcePalette; }) { return ( ); } function StatusBadge({ status, palette, }: { status: DocumentStatus; palette: SourcePalette; }) { const tone = status === "ready" ? palette.status.ready : status === "processing" || status === "uploaded" ? palette.status.processing : status === "failed" ? palette.status.failed : palette.status.default; return ( {status === "ready" ? ( ); } export function SourcesView({ surface = "dark", headingLevel = "h2", }: { surface?: SourcesSurface; headingLevel?: HeadingLevel; }) { const palette = getPalette(surface); const [documents, setDocuments] = useState([]); const [papers, setPapers] = useState([]); const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(true); const [query, setQuery] = useState(""); const [filter, setFilter] = useState("all"); const [confirmDeleteId, setConfirmDeleteId] = useState(null); const [deletingId, setDeletingId] = useState(null); const [deleteError, setDeleteError] = useState<{ id: string; message: string; } | null>(null); const requestRef = useRef(0); const canDeleteSources = !shouldShowDemoSources() && hasLiveSession(); const loadSources = useCallback(async () => { const requestId = ++requestRef.current; setIsLoading(true); setError(null); try { const { docs, papers: nextPapers } = await fetchSources(); if (requestId !== requestRef.current) return; setDocuments(docs); setPapers(nextPapers); } catch { if (requestId !== requestRef.current) return; if (shouldShowDemoSources()) { setDocuments(buildDemoSeedDocuments()); setPapers(getLocalPreviousPapers()); } else { setError("Could not load your sources. Check your connection and retry."); } } finally { if (requestId === requestRef.current) setIsLoading(false); } }, []); useEffect(() => { const timeoutId = window.setTimeout(() => void loadSources(), 0); return () => { window.clearTimeout(timeoutId); requestRef.current += 1; }; }, [loadSources]); const normalizedQuery = query.trim().toLowerCase(); const filteredDocs = documents .filter((document) => { const searchableText = [ document.title, document.file_name, document.subject, document.chapter, ] .filter(Boolean) .join(" ") .toLowerCase(); return ( (!normalizedQuery || searchableText.includes(normalizedQuery)) && matchesFilter(document, filter) ); }) .sort((a, b) => b.created_at.localeCompare(a.created_at)); async function removeSource(documentId: string) { setDeletingId(documentId); setDeleteError(null); try { await deleteDocument(documentId); setDocuments((current) => current.filter((document) => document.id !== documentId), ); setConfirmDeleteId(null); } catch { setDeleteError({ id: documentId, message: "Could not delete this source. It is still available. Check your connection and retry.", }); setConfirmDeleteId(documentId); } finally { setDeletingId(null); } } const Heading = headingLevel === "h1" ? "h1" : "h2"; const filterLabel = FILTERS.find((item) => item.value === filter)?.label; const hasDocuments = documents.length > 0; return (
Sources

Uploaded PDFs and TXT files available to your study workspace. Study actions unlock after a source is ready.

{FILTERS.map((item) => ( setFilter(item.value)} palette={palette} /> ))}

{isLoading ? "Loading sources." : error ? error : normalizedQuery || filter !== "all" ? `${filteredDocs.length} ${filteredDocs.length === 1 ? "source" : "sources"} shown.` : `${documents.length} ${documents.length === 1 ? "source" : "sources"} available.`}

{error ? (

{hasDocuments ? "Could not refresh your sources. Showing the last available list." : error}

) : null}

Study materials · {filteredDocs.length}

Full library
{isLoading && !hasDocuments ? (
Loading your study material {Array.from({ length: 4 }).map((_, index) => ( ) : null} {!isLoading && filteredDocs.length === 0 && (!error || hasDocuments) ? (

{normalizedQuery ? `No sources match “${query.trim()}”` : filter !== "all" ? `No ${filterLabel?.toLowerCase()} available` : "No uploaded sources yet"}

{normalizedQuery || filter !== "all" ? "Try a different search or clear the material filter." : "Upload a selectable-text PDF or TXT file. Images and scanned PDFs are not supported yet."}

{normalizedQuery || filter !== "all" ? ( ) : (
) : null} {!isLoading && filteredDocs.length > 0 ? (
{filteredDocs.map((document) => { const sourceTitle = document.title?.trim() || document.file_name || "Untitled source"; const isReady = document.status === "ready"; return (

{sourceTitle}

{[document.subject, document.chapter].filter(Boolean).join(" · ") || "General study material"}

{getDocumentMeta(document)}
{isReady ? ( <> Ask DocDoe
{confirmDeleteId === document.id ? (
void removeSource(document.id)} onCancel={() => { if (deletingId) return; setConfirmDeleteId(null); setDeleteError(null); }} />
) : null}
); })}
) : null}
); }