Download src/components/study-chat/SourceRail.tsx from asnannp/DocDoeAI: direct link, hf CLI and curl.
- Browser
- Download file 9.47 kB
-
https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/components/study-chat/SourceRail.tsx
- Command line
-
hf download hf://spaces/asnannp/DocDoeAI/src/components/study-chat/SourceRail.tsx
-
curl -L -o SourceRail.tsx https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/components/study-chat/SourceRail.tsx
9.47 kB
| "use client"; | |
| import { useMemo } from "react"; | |
| import { FileText, Upload, Edit2 } from "lucide-react"; | |
| import type { ApiDocument } from "@/lib/api/types"; | |
| import { cn } from "@/lib/utils"; | |
| import { documentKindLabel, documentStatusLabel } from "./utils"; | |
| import { WorkspaceSearch } from "./WorkspacePanelPrimitives"; | |
| function formatMaterialType(materialType: string | null | undefined): string { | |
| if (!materialType || materialType === "unknown") return "Unclassified"; | |
| return materialType.replace(/_/g, " ").replace(/\b\w/g, (char) => char.toUpperCase()); | |
| } | |
| function getMaterialTypeBadgeColor(materialType: string | null | undefined): string { | |
| if (!materialType || materialType === "unknown") { | |
| return "border-white/12 bg-white/[0.04] text-white/46"; | |
| } | |
| const colors: Record<string, string> = { | |
| notes: "border-white/14 bg-white/[0.06] text-white/72", | |
| chapter: "border-blue-300/16 bg-blue-400/[0.08] text-blue-200/80", | |
| textbook: "border-emerald-300/16 bg-emerald-400/[0.08] text-emerald-200/80", | |
| handwritten_notes: "border-amber-300/16 bg-amber-400/[0.08] text-amber-200/80", | |
| question_paper: "border-rose-300/16 bg-rose-400/[0.08] text-rose-200/80", | |
| pyq_collection: "border-orange-300/16 bg-orange-400/[0.08] text-orange-200/80", | |
| syllabus: "border-cyan-300/16 bg-cyan-400/[0.08] text-cyan-200/80", | |
| formula_sheet: "border-pink-300/16 bg-pink-400/[0.08] text-pink-200/80", | |
| mixed: "border-white/14 bg-white/[0.06] text-white/72", | |
| }; | |
| return colors[materialType] || colors.notes; | |
| } | |
| function formatEducationSummary(document: ApiDocument): string { | |
| const status = document.education_extraction_status; | |
| const warnings = document.education_warnings_json ?? []; | |
| if (status === "extraction_failed") return "Extraction failed"; | |
| if (status === "needs_review") return warnings[0] ? `Needs review - ${warnings[0]}` : "Needs review"; | |
| if (document.material_type === "syllabus") { | |
| const count = document.syllabus_items_count ?? 0; | |
| return count > 0 ? `Syllabus - ${count} chapter${count === 1 ? "" : "s"} extracted` : "Syllabus - extracting topics"; | |
| } | |
| if (document.material_type === "question_paper") { | |
| const count = document.pyq_questions_count ?? 0; | |
| const years = (document.pyq_years_json ?? []).join(", "); | |
| if (count > 0) { | |
| return `PYQ paper${years ? ` - ${years}` : ""} - ${count} question${count === 1 ? "" : "s"} extracted`; | |
| } | |
| return "Question paper - no questions extracted yet"; | |
| } | |
| if (document.status === "ready") return "Generic notes - no PYQ data"; | |
| return documentStatusLabel(document.status); | |
| } | |
| type SourceRailProps = { | |
| documents: ApiDocument[]; | |
| selectedSourceIds: string[]; | |
| isLoading: boolean; | |
| search: string; | |
| onSearchChange: (value: string) => void; | |
| onToggleSource: (id: string) => void; | |
| onUpload: () => void; | |
| onEditClassification?: (documentId: string, documentTitle: string, currentMaterialType: string | null) => void; | |
| }; | |
| export function SourceRail({ | |
| documents, | |
| selectedSourceIds, | |
| isLoading, | |
| search, | |
| onSearchChange, | |
| onToggleSource, | |
| onUpload, | |
| onEditClassification, | |
| }: SourceRailProps) { | |
| const filtered = useMemo(() => { | |
| const query = search.trim().toLowerCase(); | |
| if (!query) return documents; | |
| return documents.filter((document) => | |
| [document.title, document.subject, document.chapter, document.syllabus, document.file_type, document.material_type] | |
| .filter(Boolean) | |
| .some((value) => String(value).toLowerCase().includes(query)), | |
| ); | |
| }, [documents, search]); | |
| return ( | |
| <div className="flex h-full min-h-0 flex-col rounded-[1.25rem] border border-white/[0.075] bg-[#080808]/88"> | |
| <div className="border-b border-white/[0.06] p-4"> | |
| <div className="flex items-start justify-between gap-3"> | |
| <div> | |
| <p className="text-sm font-semibold text-white">Sources</p> | |
| <p className="mt-0.5 text-xs text-white/36"> | |
| {documents.length ? `${documents.filter((document) => document.status === "ready").length} ready of ${documents.length}` : "Upload study material"} | |
| </p> | |
| </div> | |
| <button | |
| type="button" | |
| onClick={onUpload} | |
| className="grid size-9 shrink-0 place-items-center rounded-xl bg-white text-black transition hover:bg-white/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/28" | |
| aria-label="Upload source" | |
| > | |
| <Upload className="size-4" /> | |
| </button> | |
| </div> | |
| <div className="mt-3"> | |
| <WorkspaceSearch value={search} onChange={onSearchChange} placeholder="Search sources" /> | |
| </div> | |
| </div> | |
| <div className="min-h-0 flex-1 overflow-y-auto p-3 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"> | |
| {isLoading ? ( | |
| <div className="space-y-2"> | |
| {Array.from({ length: 5 }).map((_, index) => ( | |
| <div key={index} className="h-16 animate-pulse rounded-2xl border border-white/[0.06] bg-white/[0.025]" /> | |
| ))} | |
| </div> | |
| ) : filtered.length === 0 ? ( | |
| <div className="flex min-h-[14rem] flex-col items-center justify-center rounded-2xl border border-dashed border-white/[0.08] px-5 text-center"> | |
| <FileText className="size-7 text-white/32" /> | |
| <p className="mt-3 text-sm font-semibold text-white/72">No sources yet</p> | |
| <p className="mt-1 text-xs leading-5 text-white/36">Upload a selectable-text PDF or TXT file, or paste syllabus text.</p> | |
| <button | |
| type="button" | |
| onClick={onUpload} | |
| className="mt-4 inline-flex min-h-10 items-center gap-2 rounded-xl bg-white px-3 text-xs font-bold text-black transition hover:bg-white/90" | |
| > | |
| <Upload className="size-3.5" /> | |
| Add source | |
| </button> | |
| </div> | |
| ) : ( | |
| <div className="space-y-2"> | |
| {filtered.map((document) => { | |
| const active = selectedSourceIds.includes(document.id); | |
| return ( | |
| <div | |
| key={document.id} | |
| className={cn( | |
| "group flex w-full min-w-0 items-start gap-2 rounded-2xl border p-3 text-left transition", | |
| active | |
| ? "border-white/[0.22] bg-white/[0.08]" | |
| : "border-white/[0.065] bg-white/[0.025] hover:border-white/[0.14] hover:bg-white/[0.045]", | |
| )} | |
| > | |
| <button | |
| type="button" | |
| onClick={() => onToggleSource(document.id)} | |
| className="flex min-w-0 flex-1 items-start gap-3 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/20" | |
| > | |
| <span className={cn("mt-0.5 grid size-9 shrink-0 place-items-center rounded-xl border", active ? "border-white/20 bg-white text-black" : "border-white/[0.08] bg-white/[0.035] text-white/58")}> | |
| <FileText className="size-4" /> | |
| </span> | |
| <span className="min-w-0 flex-1"> | |
| <span className="line-clamp-2 text-sm font-semibold leading-5 text-white/86">{document.title}</span> | |
| <span className="mt-1 flex items-center flex-wrap gap-1.5 text-[11px] text-white/36"> | |
| <span className="truncate">{document.subject || documentKindLabel(document)}</span> | |
| <span className="size-1 rounded-full bg-white/24" /> | |
| <span>{documentStatusLabel(document.status)}</span> | |
| {document.material_type && document.material_type !== "unknown" && ( | |
| <> | |
| <span className="size-1 rounded-full bg-white/24" /> | |
| <span className={cn( | |
| "rounded-full border px-1.5 py-0.5 text-[9px] font-medium", | |
| getMaterialTypeBadgeColor(document.material_type) | |
| )}> | |
| {formatMaterialType(document.material_type)} | |
| </span> | |
| </> | |
| )} | |
| </span> | |
| <span className="mt-1 block truncate text-[10.5px] leading-4 text-white/28"> | |
| {formatEducationSummary(document)} | |
| </span> | |
| </span> | |
| </button> | |
| {document.status === "ready" && onEditClassification && ( | |
| <button | |
| type="button" | |
| onClick={() => onEditClassification(document.id, document.title, document.material_type ?? null)} | |
| className="mt-0.5 grid size-7 shrink-0 place-items-center rounded-lg border border-white/[0.08] bg-white/[0.02] text-white/50 transition-all hover:border-white/20 hover:bg-white/[0.06] hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/20" | |
| aria-label={`Edit classification for ${document.title}`} | |
| > | |
| <Edit2 className="size-3" strokeWidth={2} /> | |
| </button> | |
| )} | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } | |