import { useState } from "react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Textarea } from "@/components/ui/textarea"; import { Plus, ChevronRight, BookOpen, Layers, FileText, Sparkles, Loader2, Trash2, X, Pencil, Check, Library, GraduationCap, Link2, } from "lucide-react"; import { useSubjects, useTaxonomyTree } from "@/hooks/queries"; import { taxonomyService } from "@/services/taxonomy.service"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useToast } from "@/hooks/use-toast"; import { useNavigate } from "react-router-dom"; import type { Domain, Topic } from "@/types"; function RelBadge({ count, label, icon: Icon, onClick }: { count: number; label: string; icon: React.ElementType; onClick?: () => void }) { return ( 0 ? "default" : "outline"} className={`text-xs gap-1 ${onClick ? "cursor-pointer hover:bg-accent" : ""} ${count === 0 ? "text-muted-foreground" : ""}`} onClick={(e) => { e.stopPropagation(); onClick?.(); }} > {count} {label} ); } export default function TaxonomyManager() { const { toast } = useToast(); const qc = useQueryClient(); const navigate = useNavigate(); const { data: rawSubjects, isLoading } = useSubjects(); const subjects = Array.isArray(rawSubjects) ? rawSubjects : []; const [selectedSubjectId, setSelectedSubjectId] = useState(null); const { data: tree, isLoading: loadingTree } = useTaxonomyTree(selectedSubjectId ?? 0); const [showAddSubject, setShowAddSubject] = useState(false); const [newSubjectName, setNewSubjectName] = useState(""); const [newSubjectCode, setNewSubjectCode] = useState(""); const [showAddDomain, setShowAddDomain] = useState(false); const [newDomainName, setNewDomainName] = useState(""); const [showAddTopic, setShowAddTopic] = useState(null); const [newTopicName, setNewTopicName] = useState(""); const [newTopicDifficulty, setNewTopicDifficulty] = useState<"easy" | "medium" | "hard">("medium"); const [newTopicHours, setNewTopicHours] = useState("1"); const [editingSubjectId, setEditingSubjectId] = useState(null); const [editSubjectName, setEditSubjectName] = useState(""); const [editSubjectCode, setEditSubjectCode] = useState(""); const [editingDomainId, setEditingDomainId] = useState(null); const [editDomainName, setEditDomainName] = useState(""); const [editingTopicId, setEditingTopicId] = useState(null); const [editTopicName, setEditTopicName] = useState(""); const [editTopicDifficulty, setEditTopicDifficulty] = useState<"easy" | "medium" | "hard">("medium"); const [editTopicHours, setEditTopicHours] = useState("1"); const [editTopicDesc, setEditTopicDesc] = useState(""); const invalidate = () => qc.invalidateQueries({ queryKey: ["taxonomy"] }); const createSubjectMutation = useMutation({ mutationFn: () => taxonomyService.createSubject({ name: newSubjectName, code: newSubjectCode, is_active: true, mastery_threshold: 80, grading_scale: "percentage", diagnostic_config: { questions_per_domain: 5, total_question_cap: 30, time_limit_minutes: 45, starting_difficulty: "medium", mastery_per_correct: 10, mastery_per_incorrect: -5 } }), onSuccess: () => { invalidate(); toast({ title: "Subject Created" }); setShowAddSubject(false); setNewSubjectName(""); setNewSubjectCode(""); }, onError: () => toast({ title: "Error", description: "Failed to create subject", variant: "destructive" }), }); const updateSubjectMutation = useMutation({ mutationFn: ({ id, data }: { id: number; data: Partial<{ name: string; code: string }> }) => taxonomyService.updateSubject(id, data), onSuccess: () => { invalidate(); toast({ title: "Subject updated" }); setEditingSubjectId(null); }, onError: () => toast({ title: "Error", description: "Failed to update subject", variant: "destructive" }), }); const deleteSubjectMutation = useMutation({ mutationFn: (id: number) => taxonomyService.deleteSubject(id), onSuccess: (_d, id) => { invalidate(); if (selectedSubjectId === id) setSelectedSubjectId(null); toast({ title: "Subject deleted" }); }, onError: () => toast({ title: "Error", description: "Failed to delete subject", variant: "destructive" }), }); const createDomainMutation = useMutation({ mutationFn: (data: Partial) => taxonomyService.createDomain(data), onSuccess: () => { invalidate(); setShowAddDomain(false); setNewDomainName(""); toast({ title: "Domain created" }); }, onError: () => toast({ title: "Error", description: "Failed to create domain", variant: "destructive" }), }); const updateDomainMutation = useMutation({ mutationFn: ({ id, data }: { id: number; data: Partial }) => taxonomyService.updateDomain(id, data), onSuccess: () => { invalidate(); toast({ title: "Domain updated" }); setEditingDomainId(null); }, onError: () => toast({ title: "Error", description: "Failed to update domain", variant: "destructive" }), }); const deleteDomainMutation = useMutation({ mutationFn: (id: number) => taxonomyService.deleteDomain(id), onSuccess: () => { invalidate(); toast({ title: "Domain deleted" }); }, onError: () => toast({ title: "Error", description: "Failed to delete domain", variant: "destructive" }), }); const aiSuggestMutation = useMutation({ mutationFn: (domainId: number) => taxonomyService.aiSuggestTopics(domainId), onSuccess: (data) => toast({ title: "AI Suggestions", description: `${data.suggestions.length} topics suggested.` }), onError: () => toast({ title: "Error", description: "AI suggestion failed", variant: "destructive" }), }); const createTopicMutation = useMutation({ mutationFn: (data: Partial) => taxonomyService.createTopic(data), onSuccess: () => { invalidate(); setShowAddTopic(null); setNewTopicName(""); toast({ title: "Topic created" }); }, onError: () => toast({ title: "Error", description: "Failed to create topic", variant: "destructive" }), }); const updateTopicMutation = useMutation({ mutationFn: ({ id, data }: { id: number; data: Partial }) => taxonomyService.updateTopic(id, data), onSuccess: () => { invalidate(); toast({ title: "Topic updated" }); setEditingTopicId(null); }, onError: () => toast({ title: "Error", description: "Failed to update topic", variant: "destructive" }), }); const deleteTopicMutation = useMutation({ mutationFn: (id: number) => taxonomyService.deleteTopic(id), onSuccess: () => { invalidate(); toast({ title: "Topic deleted" }); }, onError: () => toast({ title: "Error", description: "Failed to delete topic", variant: "destructive" }), }); const startEditSubject = (s: { id: number; name: string; code: string }) => { setEditingSubjectId(s.id); setEditSubjectName(s.name); setEditSubjectCode(s.code); }; const startEditDomain = (d: { id: number; name: string }) => { setEditingDomainId(d.id); setEditDomainName(d.name); }; const startEditTopic = (t: { id: number; name: string; difficulty_level?: string; estimated_hours?: number; description?: string }) => { setEditingTopicId(t.id); setEditTopicName(t.name); setEditTopicDifficulty((t.difficulty_level as "easy" | "medium" | "hard") ?? "medium"); setEditTopicHours(String(t.estimated_hours ?? 1)); setEditTopicDesc(t.description ?? ""); }; if (isLoading) return
; return (

Taxonomy Manager

Manage subjects, domains, topics, and learning objectives.

Create New Subject
setNewSubjectName(e.target.value)} />
setNewSubjectCode(e.target.value)} />
{/* Subjects sidebar */}

Subjects

{subjects.map(s => (
{editingSubjectId === s.id ? (
setEditSubjectName(e.target.value)} className="h-7 text-sm" placeholder="Name" /> setEditSubjectCode(e.target.value)} className="h-7 text-sm" placeholder="Code" />
) : ( <>
navigate("/admin/courses")} /> navigate("/admin/resources")} />
)}
))} {subjects.length === 0 &&

No subjects yet.

}
{/* Domain + Topic tree */}
{!selectedSubjectId ? ( Select a subject to view its taxonomy tree. ) : loadingTree ? (
) : tree ? (
{/* Relationship summary for selected subject */} {(() => { const sel = subjects.find(s => s.id === selectedSubjectId); if (!sel) return null; return (
Relationships: · ·
{sel.domain_count ?? 0} Domains
·
{sel.topic_count ?? 0} Topics
); })()} {(Array.isArray(tree.domains) ? tree.domains : []).map(domain => (
{editingDomainId === domain.id ? (
e.stopPropagation()}> setEditDomainName(e.target.value)} className="h-7 text-sm w-48" />
) : ( {domain.name} )} {(Array.isArray(domain.topics) ? domain.topics : []).length} topics
{editingDomainId !== domain.id && ( )}
{showAddTopic === domain.id && (
setNewTopicName(e.target.value)} className="h-8 text-sm" /> setNewTopicHours(e.target.value)} className="w-16 h-8 text-sm" />
)} {(Array.isArray(domain.topics) ? domain.topics : []).map(topic => (
{editingTopicId === topic.id ? (
setEditTopicName(e.target.value)} className="h-7 text-sm flex-1" placeholder="Topic name" /> setEditTopicHours(e.target.value)} className="w-16 h-7 text-sm" placeholder="Hrs" />