DocDoeAI / src /components /study-chat /SourceClassificationModal.tsx
asnannp's picture
deploy: sync backend to Space root (learn-lesson HF cache fix)
65d68f5
Raw History Blame Contribute Delete
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">&ldquo;{documentTitle}&rdquo;</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>
);
}