"use client"; import { useCallback, useRef, useState } from "react"; import { CheckCircle2, Clock, File, FileText, Image as ImageIcon, Link, Loader2, Plus, Search, Upload, XCircle, } from "lucide-react"; import { cn } from "@/lib/utils"; import { uploadDocument } from "@/lib/api/documents"; import type { DocumentStatus } from "@/lib/api/types"; import { getStudentFacingSourceTitle } from "@/lib/ui/displayNames"; import { MATERIAL_UPLOAD_ACCEPT, validateStudentUpload, } from "@/lib/uploads/document-upload-contract"; import { materialTypeLabel, sourceReadinessLabel } from "./SourceTrust"; import type { MaterialType } from "./workspaceTypes"; export type SourceDoc = { id: string; title: string; status: DocumentStatus; subject: string; syllabus?: string | null; file_type: string; chunk_count?: number; material_type?: MaterialType | null; source_type?: string | null; }; type SourcesPanelProps = { className?: string; subject: string; documents: SourceDoc[]; activeDocumentId?: string | null; onDocumentSelect: (doc: SourceDoc | null) => void; onRefresh?: () => void; isLoading?: boolean; onUploadClick?: () => void; }; const VISIBLE_COUNT = 4; export function SourcesPanel({ className, subject, documents, activeDocumentId, onDocumentSelect, onRefresh, isLoading = false, onUploadClick, }: SourcesPanelProps) { const [search, setSearch] = useState(""); const [showAll, setShowAll] = useState(false); const [isUploading, setIsUploading] = useState(false); const [uploadError, setUploadError] = useState(null); const fileInputRef = useRef(null); const triggerUpload = useCallback(() => { setUploadError(null); if (onUploadClick) { onUploadClick(); return; } fileInputRef.current?.click(); }, [onUploadClick]); const handleFileChange = useCallback( async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; e.target.value = ""; const validation = validateStudentUpload(file, "material"); if (!validation.ok) { setUploadError(validation.message); return; } setIsUploading(true); setUploadError(null); try { await uploadDocument({ file, title: file.name.replace(/\.[^/.]+$/, ""), subject, }); onRefresh?.(); } catch { setUploadError("Couldn't upload this file. Try another one."); } finally { setIsUploading(false); } }, [subject, onRefresh], ); const filtered = sortDocuments(filterBySearch(documents, search), activeDocumentId); const visible = showAll ? filtered : filtered.slice(0, VISIBLE_COUNT); const visibleGroups = groupByStatus(visible); const hiddenCount = Math.max(0, filtered.length - VISIBLE_COUNT); return ( ); } // ─── Source card ────────────────────────────────────────────────────────────── function SourceCard({ doc, isActive, onSelect, }: { doc: SourceDoc; isActive: boolean; onSelect: () => void; }) { const iconClassName = cn( "size-4", isActive ? "text-white" : "text-white/40 group-hover:text-white/60", ); return ( ); } // ─── Status badge ───────────────────────────────────────────────────────────── function StatusBadge({ source }: { source: SourceDoc }) { const label = sourceReadinessLabel(source); if (label === "Ready") { return ( Ready ); } if (label === "Processing") { return ( Processing ); } if (label === "Failed") { return ( Failed ); } return ( {label} ); } // ─── Loading skeleton ───────────────────────────────────────────────────────── function LoadingSkeleton() { return (
{[1, 2, 3].map((i) => (
))}
); } // ─── Empty state ────────────────────────────────────────────────────────────── function EmptyState({ onUpload }: { onUpload: () => void }) { return (

Add your first study source

Add notes, a chapter, syllabus, or a question paper. DocDoe will find study signals from your material.

); } // ─── Helpers ────────────────────────────────────────────────────────────────── function FileTypeIcon({ fileType, className }: { fileType: string; className?: string }) { if (!fileType) return ; const t = fileType.toLowerCase(); if (t.includes("pdf")) return ; if (t.startsWith("image/")) return ; if (t.includes("html") || t.includes("url") || t.includes("link")) { return ; } return ; } function filterBySearch(docs: SourceDoc[], search: string): SourceDoc[] { if (!search.trim()) return docs; const q = search.toLowerCase(); return docs.filter( (d) => d.title?.toLowerCase().includes(q) || d.subject?.toLowerCase().includes(q) || materialTypeLabel(d.material_type).toLowerCase().includes(q), ); } function sortDocuments(docs: SourceDoc[], activeDocumentId: string | null | undefined): SourceDoc[] { const statusOrder: Record = { ready: 0, processing: 1, uploaded: 2, failed: 3 }; return [...docs].sort((a, b) => { if (a.id === activeDocumentId) return -1; if (b.id === activeDocumentId) return 1; return (statusOrder[a.status] ?? 2) - (statusOrder[b.status] ?? 2); }); } function groupByStatus(docs: SourceDoc[]): Array<{ label: string; docs: SourceDoc[] }> { const groups = [ { label: "Ready", docs: docs.filter((doc) => sourceReadinessLabel(doc) === "Ready") }, { label: "Processing", docs: docs.filter((doc) => sourceReadinessLabel(doc) === "Processing"), }, { label: "Needs classification", docs: docs.filter((doc) => sourceReadinessLabel(doc) === "Needs classification"), }, { label: "Not study material", docs: docs.filter((doc) => sourceReadinessLabel(doc) === "Not study material"), }, { label: "Failed", docs: docs.filter((doc) => sourceReadinessLabel(doc) === "Failed") }, ]; return groups.filter((group) => group.docs.length > 0); }