'use client'; import { useState, useEffect, useCallback, useRef, useMemo } from 'react'; import { Code, Loader2, RefreshCw, Filter, ChevronDown, ChevronLeft, ChevronRight, CheckCircle2, Circle, Search, X, Image as ImageIcon, Database, } from 'lucide-react'; import { clsx } from 'clsx'; import { useDataset } from '@/lib/dataset/DatasetProvider'; import { TASK_LABELS, CATEGORY_LABELS } from '@/config/constants'; import type { CodingProblem, TaskType, Category } from '@/types'; interface ProblemListProps { onSelectProblem: (problem: CodingProblem) => void; selectedProblemId?: string; solvedProblems: Set; } const ITEMS_PER_PAGE = 25; type Split = 'validation' | 'test'; export function ProblemList({ onSelectProblem, selectedProblemId, solvedProblems, }: ProblemListProps) { const { isLoading: isDatasetLoading, loadedSplits, splitCounts, filterExamples } = useDataset(); const [typeFilter, setTypeFilter] = useState('all'); const [categoryFilter, setCategoryFilter] = useState('all'); const [multimodalFilter, setMultimodalFilter] = useState<'all' | 'with' | 'without'>('all'); const [showFilters, setShowFilters] = useState(false); const [searchQuery, setSearchQuery] = useState(''); const [debouncedSearch, setDebouncedSearch] = useState(''); const [split, setSplit] = useState('test'); const [currentPage, setCurrentPage] = useState(0); const scrollContainerRef = useRef(null); const searchInputRef = useRef(null); // Debounce search useEffect(() => { const timer = setTimeout(() => { setDebouncedSearch(searchQuery); setCurrentPage(0); }, 300); return () => clearTimeout(timer); }, [searchQuery]); // Reset page when filters change useEffect(() => { setCurrentPage(0); }, [split, typeFilter, categoryFilter, multimodalFilter, debouncedSearch]); // Filter locally loaded data const { problems, totalProblems } = useMemo(() => { if (!loadedSplits.has(split)) { return { problems: [], totalProblems: 0 }; } const filters: { type?: TaskType; category?: Category; hasImage?: boolean; search?: string; codingOnly: boolean; } = { codingOnly: true }; if (typeFilter !== 'all') filters.type = typeFilter; if (categoryFilter !== 'all') filters.category = categoryFilter; if (multimodalFilter === 'with') filters.hasImage = true; else if (multimodalFilter === 'without') filters.hasImage = false; if (debouncedSearch) filters.search = debouncedSearch; const result = filterExamples(split, filters, ITEMS_PER_PAGE, currentPage * ITEMS_PER_PAGE); const codingProblems = result.examples.filter( (e): e is CodingProblem => e.testCode !== undefined && e.entryPoint !== undefined && (e.type === 'function_completion' || e.type === 'code_generation') ); return { problems: codingProblems, totalProblems: result.total }; }, [loadedSplits, split, filterExamples, typeFilter, categoryFilter, multimodalFilter, debouncedSearch, currentPage]); // Scroll to top on page change useEffect(() => { if (scrollContainerRef.current) { scrollContainerRef.current.scrollTop = 0; } }, [currentPage]); const totalPages = Math.ceil(totalProblems / ITEMS_PER_PAGE); const stats = useMemo(() => ({ total: totalProblems, solved: problems.filter((p) => solvedProblems.has(p.id)).length, currentPageSolved: problems.filter((p) => solvedProblems.has(p.id)).length, displayed: problems.length, }), [problems, solvedProblems, totalProblems]); const truncateText = (text: string, maxLength: number) => { if (text.length <= maxLength) return text; return text.substring(0, maxLength).trim() + '...'; }; const clearSearch = () => { setSearchQuery(''); searchInputRef.current?.focus(); }; const badgeColors: Record = { function_completion: 'bg-emerald-900/30 text-emerald-400 border-emerald-700/30', code_generation: 'bg-blue-900/30 text-blue-400 border-blue-700/30', }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Escape') { clearSearch(); } }; const isLoading = isDatasetLoading || !loadedSplits.has(split); return (
{/* Header */}

Practice Problems

{isLoading && ( )}
{/* Split Selector */}
{(['test', 'validation'] as Split[]).map((s) => ( ))}
{/* Search */}
setSearchQuery(e.target.value)} onKeyDown={handleKeyDown} placeholder="Search problems..." className="w-full bg-zinc-800/80 border border-zinc-700/50 rounded-lg pl-9 pr-8 py-2 text-sm text-zinc-300 placeholder:text-zinc-600 focus:outline-none focus:ring-1 focus:ring-teal-600/50 focus:border-teal-700/50" /> {searchQuery && ( )}
{/* Stats */}
{stats.currentPageSolved} solved | {stats.total} problems {debouncedSearch && ( <> | "{debouncedSearch}" )}
{/* Filters Toggle */} {/* Filter Options */} {showFilters && (
{/* Clear Filters */} {(typeFilter !== 'all' || categoryFilter !== 'all' || multimodalFilter !== 'all') && ( )}
)}
{/* Problem List */}
{isLoading ? (
Loading problems...
) : problems.length === 0 ? (

No problems match your filters

{debouncedSearch && ( )}
) : (

Showing {currentPage * ITEMS_PER_PAGE + 1}–{Math.min((currentPage + 1) * ITEMS_PER_PAGE, totalProblems)} of {totalProblems}

{problems.map((problem, idx) => { const isSolved = solvedProblems.has(problem.id); const isSelected = problem.id === selectedProblemId; const taskConfig = TASK_LABELS[problem.type]; const globalIndex = currentPage * ITEMS_PER_PAGE + idx + 1; return ( ); })}
)}
{/* Pagination - Fixed overflow */} {totalPages > 1 && !isLoading && (
{(() => { const maxVisible = 3; const pages: (number | 'ellipsis')[] = []; if (totalPages <= maxVisible + 2) { // Show all pages for (let i = 0; i < totalPages; i++) pages.push(i); } else { // Always show first page pages.push(0); if (currentPage > 2) { pages.push('ellipsis'); } // Show pages around current const start = Math.max(1, currentPage - 1); const end = Math.min(totalPages - 2, currentPage + 1); for (let i = start; i <= end; i++) { if (!pages.includes(i)) pages.push(i); } if (currentPage < totalPages - 3) { pages.push('ellipsis'); } // Always show last page if (!pages.includes(totalPages - 1)) { pages.push(totalPages - 1); } } return pages.map((page, idx) => { if (page === 'ellipsis') { return ( ); } return ( ); }); })()}
)}
); }