Download src/components/study-chat/SourceClassificationModal.tsx from asnannp/DocDoeAI: direct link, hf CLI and curl.
- Browser
- Download file 7.15 kB
-
https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/components/study-chat/SourceClassificationModal.tsx
- Command line
-
hf download hf://spaces/asnannp/DocDoeAI/src/components/study-chat/SourceClassificationModal.tsx
-
curl -L -o SourceClassificationModal.tsx https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/components/study-chat/SourceClassificationModal.tsx
7.15 kB
| "use client"; | |
| import { useEffect, useState } from "react"; | |
| import { FileText, BookOpen, PenTool, ClipboardList, Library, GraduationCap, Sparkles } from "lucide-react"; | |
| import { Modal, PrimaryButton, SecondaryButton } from "@/components/dashboard/dashboardPrimitives"; | |
| import { reclassifyDocument } from "@/lib/api/documents"; | |
| import { normalizeApiError } from "@/lib/api/client"; | |
| import { cn } from "@/lib/utils"; | |
| const MATERIAL_TYPE_OPTIONS = [ | |
| { value: "notes", label: "Notes", icon: FileText, description: "Typed class notes or study notes" }, | |
| { value: "chapter", label: "Chapter PDF", icon: BookOpen, description: "Textbook chapter, reference material" }, | |
| { value: "textbook", label: "Textbook", icon: Library, description: "Full textbook or reference book" }, | |
| { value: "handwritten_notes", label: "Digitized Notes", icon: PenTool, description: "Handwritten notes already converted to readable text" }, | |
| { value: "question_paper", label: "Question Paper", icon: ClipboardList, description: "Single exam or test paper" }, | |
| { value: "syllabus", label: "Syllabus", icon: GraduationCap, description: "Course syllabus or curriculum" }, | |
| ] as const; | |
| type MaterialType = typeof MATERIAL_TYPE_OPTIONS[number]["value"]; | |
| type SourceClassificationModalProps = { | |
| isOpen: boolean; | |
| onClose: () => void; | |
| documentId: string; | |
| documentTitle: string; | |
| currentMaterialType?: string | null; | |
| onClassificationComplete: (materialType: string) => void; | |
| autoDetectedType?: string | null; | |
| }; | |
| export function SourceClassificationModal({ | |
| isOpen, | |
| onClose, | |
| documentId, | |
| documentTitle, | |
| currentMaterialType, | |
| onClassificationComplete, | |
| autoDetectedType, | |
| }: SourceClassificationModalProps) { | |
| const [selectedType, setSelectedType] = useState<MaterialType | null>( | |
| (currentMaterialType || autoDetectedType || null) as MaterialType | null | |
| ); | |
| const [isSaving, setIsSaving] = useState(false); | |
| const [error, setError] = useState<string | null>(null); | |
| useEffect(() => { | |
| if (!isOpen) return; | |
| const timeoutId = window.setTimeout(() => { | |
| setSelectedType((currentMaterialType || autoDetectedType || null) as MaterialType | null); | |
| setError(null); | |
| }, 0); | |
| return () => window.clearTimeout(timeoutId); | |
| }, [autoDetectedType, currentMaterialType, isOpen]); | |
| async function handleSave() { | |
| if (!selectedType) { | |
| setError("Please select a material type"); | |
| return; | |
| } | |
| setIsSaving(true); | |
| setError(null); | |
| try { | |
| await reclassifyDocument(documentId, selectedType); | |
| onClassificationComplete(selectedType); | |
| onClose(); | |
| } catch (err) { | |
| setError(normalizeApiError(err)); | |
| } finally { | |
| setIsSaving(false); | |
| } | |
| } | |
| function handleSkip() { | |
| // If auto-detected type exists, use it; otherwise use current or default to unknown | |
| const typeToUse = selectedType || autoDetectedType || currentMaterialType || "unknown"; | |
| onClassificationComplete(typeToUse); | |
| onClose(); | |
| } | |
| return ( | |
| <Modal isOpen={isOpen} onClose={onClose} title="What type of material is this?"> | |
| <div className="space-y-5"> | |
| <div className="text-center"> | |
| <p className="text-[13px] text-white/40 leading-relaxed"> | |
| Classifying <span className="font-semibold text-white/60">“{documentTitle}”</span> helps DocDoe provide better study tools. | |
| </p> | |
| </div> | |
| {autoDetectedType && !currentMaterialType && ( | |
| <div className="rounded-xl border border-white/14 bg-white/[0.06] px-4 py-3 flex items-start gap-3"> | |
| <Sparkles className="size-4 text-white/75 shrink-0 mt-0.5" strokeWidth={1.8} /> | |
| <div className="flex-1 min-w-0"> | |
| <p className="text-[12px] font-semibold text-white/90">Auto-detected as: {autoDetectedType.replace(/_/g, " ")}</p> | |
| <p className="text-[11px] text-white/60 mt-1">Confirm or select a different type below.</p> | |
| </div> | |
| </div> | |
| )} | |
| <div className="grid grid-cols-2 gap-2 max-h-[240px] overflow-y-auto"> | |
| {MATERIAL_TYPE_OPTIONS.map((option) => { | |
| const Icon = option.icon; | |
| const isSelected = selectedType === option.value; | |
| const isAutoDetected = autoDetectedType === option.value && !currentMaterialType; | |
| return ( | |
| <button | |
| key={option.value} | |
| type="button" | |
| onClick={() => setSelectedType(option.value)} | |
| className={cn( | |
| "flex flex-col items-start gap-2 rounded-xl border p-3 text-left transition-all", | |
| "hover:bg-white/[0.04] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/35", | |
| isSelected | |
| ? "border-white/35 bg-white/[0.11] shadow-[0_0_16px_rgba(255,255,255,0.08)]" | |
| : "border-white/[0.08] bg-white/[0.02]", | |
| isAutoDetected && !isSelected | |
| ? "border-white/20 bg-white/[0.05]" | |
| : "", | |
| )} | |
| > | |
| <div className="flex items-center gap-2"> | |
| <Icon | |
| className={cn( | |
| "size-4 shrink-0", | |
| isSelected ? "text-white" : "text-white/40", | |
| isAutoDetected && !isSelected ? "text-white/75" : "" | |
| )} | |
| strokeWidth={1.8} | |
| /> | |
| <span className={cn( | |
| "text-[11.5px] font-semibold", | |
| isSelected ? "text-white" : "text-white/70", | |
| isAutoDetected && !isSelected ? "text-white/80" : "" | |
| )}> | |
| {option.label} | |
| </span> | |
| {isAutoDetected && ( | |
| <span className="text-[9px] font-semibold text-white/80 px-1.5 py-0.5 rounded-full bg-white/10"> | |
| Auto | |
| </span> | |
| )} | |
| </div> | |
| <p className="text-[10px] text-white/35 line-clamp-2 leading-tight"> | |
| {option.description} | |
| </p> | |
| </button> | |
| ); | |
| })} | |
| </div> | |
| {error && ( | |
| <div className="rounded-xl border border-rose-300/16 bg-rose-500/[0.08] px-4 py-3"> | |
| <p className="text-[11px] text-rose-100/80">{error}</p> | |
| </div> | |
| )} | |
| <div className="flex items-center gap-3 pt-2"> | |
| <SecondaryButton | |
| onClick={handleSkip} | |
| disabled={isSaving} | |
| className="flex-1" | |
| > | |
| Skip for Now | |
| </SecondaryButton> | |
| <PrimaryButton | |
| onClick={handleSave} | |
| disabled={isSaving || !selectedType} | |
| className="flex-1" | |
| > | |
| {isSaving ? "Saving..." : selectedType ? "Confirm" : "Select Type"} | |
| </PrimaryButton> | |
| </div> | |
| <p className="text-[10.5px] text-white/30 text-center leading-relaxed"> | |
| You can change this later from the source card | |
| </p> | |
| </div> | |
| </Modal> | |
| ); | |
| } | |