Download src/components/kernel/KernelSelector.tsx from TechAvenger/kernel-convolution-visualizer: direct link, hf CLI and curl.
- Browser
- Download file 15.6 kB
-
https://huggingface.co/spaces/TechAvenger/kernel-convolution-visualizer/resolve/b43cf2ad48d9ffbc7ae4391ae06b1d7e327e1112/src/components/kernel/KernelSelector.tsx
- Command line
-
hf download hf://spaces/TechAvenger/kernel-convolution-visualizer@b43cf2ad48d9ffbc7ae4391ae06b1d7e327e1112/src/components/kernel/KernelSelector.tsx
-
curl -L -o KernelSelector.tsx https://huggingface.co/spaces/TechAvenger/kernel-convolution-visualizer/resolve/b43cf2ad48d9ffbc7ae4391ae06b1d7e327e1112/src/components/kernel/KernelSelector.tsx
15.6 kB
| 'use client' | |
| import { useState, useCallback, useRef, useEffect } from 'react' | |
| import { useAppStore } from '@/store' | |
| import { PREDEFINED_KERNELS, KERNEL_CATEGORIES, getKernelsByCategory, normalizeKernel } from '@/lib/kernels' | |
| import type { KernelCategory } from '@/lib/kernels' | |
| import { Button, Card, CardContent, Badge, Input } from '@/components/ui' | |
| import { cn } from '@/utils/helpers' | |
| import { Search, SlidersHorizontal, Plus, Copy, Check, RotateCcw, Sparkles } from 'lucide-react' | |
| // Preset matrices for quick start | |
| const CUSTOM_PRESETS = { | |
| identity: { | |
| name: 'Identity', | |
| matrix: [[0, 0, 0], [0, 1, 0], [0, 0, 0]], | |
| size: 3, | |
| }, | |
| edgeDetect: { | |
| name: 'Edge Detect', | |
| matrix: [[-1, -1, -1], [-1, 8, -1], [-1, -1, -1]], | |
| size: 3, | |
| }, | |
| blur: { | |
| name: 'Box Blur', | |
| matrix: [[1, 1, 1], [1, 1, 1], [1, 1, 1]], | |
| size: 3, | |
| }, | |
| sharpen: { | |
| name: 'Sharpen', | |
| matrix: [[0, -1, 0], [-1, 5, -1], [0, -1, 0]], | |
| size: 3, | |
| }, | |
| emboss: { | |
| name: 'Emboss', | |
| matrix: [[-2, -1, 0], [-1, 1, 1], [0, 1, 2]], | |
| size: 3, | |
| }, | |
| outline: { | |
| name: 'Outline', | |
| matrix: [[-1, -1, -1], [-1, 8, -1], [-1, -1, -1]], | |
| size: 3, | |
| }, | |
| } | |
| export function KernelSelector() { | |
| const { selectedKernel, setSelectedKernel, customKernelMatrix, customKernelSize, customKernelNormalize, setCustomKernel, setCustomKernelSize, setCustomKernelNormalize, updateCustomKernelCell } = useAppStore() | |
| const [activeCategory, setActiveCategory] = useState<KernelCategory>('blur') | |
| const [searchQuery, setSearchQuery] = useState('') | |
| const [showCustom, setShowCustom] = useState(false) | |
| const [copiedPreset, setCopiedPreset] = useState<string | null>(null) | |
| // Handle keyboard navigation in matrix inputs | |
| const matrixInputRefs = useRef<HTMLInputElement[][]>([]) | |
| useEffect(() => { | |
| matrixInputRefs.current = Array.from({ length: customKernelSize }, () => Array(customKernelSize).fill(null)) | |
| }, [customKernelSize]) | |
| const handleKeyDown = useCallback( | |
| (e: React.KeyboardEvent, row: number, col: number) => { | |
| const refs = matrixInputRefs.current | |
| let newRow = row | |
| let newCol = col | |
| switch (e.key) { | |
| case 'ArrowUp': | |
| e.preventDefault() | |
| newRow = Math.max(0, row - 1) | |
| break | |
| case 'ArrowDown': | |
| e.preventDefault() | |
| newRow = Math.min(customKernelSize - 1, row + 1) | |
| break | |
| case 'ArrowLeft': | |
| e.preventDefault() | |
| newCol = Math.max(0, col - 1) | |
| break | |
| case 'ArrowRight': | |
| e.preventDefault() | |
| newCol = Math.min(customKernelSize - 1, col + 1) | |
| break | |
| case 'Tab': | |
| e.preventDefault() | |
| if (e.shiftKey) { | |
| newCol = col === 0 ? (row === 0 ? customKernelSize - 1 : customKernelSize - 1) : col - 1 | |
| newRow = col === 0 ? Math.max(0, row - 1) : row | |
| } else { | |
| newCol = col === customKernelSize - 1 ? 0 : col + 1 | |
| newRow = col === customKernelSize - 1 ? Math.min(customKernelSize - 1, row + 1) : row | |
| } | |
| break | |
| case 'Enter': | |
| e.preventDefault() | |
| newCol = col === customKernelSize - 1 ? 0 : col + 1 | |
| newRow = col === customKernelSize - 1 ? Math.min(customKernelSize - 1, row + 1) : row | |
| break | |
| } | |
| if (refs[newRow]?.[newCol]) { | |
| refs[newRow][newCol]?.focus() | |
| } | |
| }, | |
| [customKernelSize] | |
| ) | |
| const handleKernelSelect = (kernel: typeof PREDEFINED_KERNELS[0]) => { | |
| setSelectedKernel(kernel) | |
| setShowCustom(false) | |
| } | |
| const handleCustomKernelSubmit = () => { | |
| const kernel = { | |
| id: `custom-${Date.now()}`, | |
| name: `Custom ${customKernelSize}×${customKernelSize}`, | |
| category: 'custom' as KernelCategory, | |
| matrix: customKernelMatrix, | |
| size: customKernelSize, | |
| normalize: customKernelNormalize, | |
| description: 'User-defined custom kernel', | |
| } | |
| setSelectedKernel(kernel) | |
| } | |
| const applyPreset = useCallback((presetKey: keyof typeof CUSTOM_PRESETS) => { | |
| const preset = CUSTOM_PRESETS[presetKey] | |
| if (!preset) return | |
| // Resize matrix if needed | |
| if (preset.size !== customKernelSize) { | |
| setCustomKernelSize(preset.size) | |
| // Wait for state update then apply matrix | |
| setTimeout(() => { | |
| const matrix = preset.matrix.map(row => [...row]) | |
| setCustomKernel(matrix, preset.size, customKernelNormalize) | |
| }, 0) | |
| } else { | |
| const matrix = preset.matrix.map(row => [...row]) | |
| setCustomKernel(matrix, preset.size, customKernelNormalize) | |
| } | |
| setCopiedPreset(presetKey) | |
| setTimeout(() => setCopiedPreset(null), 2000) | |
| }, [customKernelSize, customKernelNormalize, setCustomKernel, setCustomKernelSize]) | |
| const copyMatrix = useCallback(() => { | |
| const matrixStr = customKernelMatrix.map(row => row.join(', ')).join('\n') | |
| navigator.clipboard.writeText(matrixStr) | |
| // Show toast or visual feedback | |
| }, [customKernelMatrix]) | |
| const pasteMatrix = useCallback(async () => { | |
| try { | |
| const text = await navigator.clipboard.readText() | |
| const rows = text.trim().split('\n').map(line => line.trim().split(/[,\s]+/).map(Number)) | |
| if (rows.length > 0 && rows.every(row => row.length === rows[0].length)) { | |
| const size = rows.length | |
| if (size >= 3 && size <= 9 && size % 2 === 1) { | |
| setCustomKernelSize(size) | |
| setTimeout(() => setCustomKernel(rows, size, customKernelNormalize), 0) | |
| } | |
| } | |
| } catch { | |
| // Clipboard access denied or invalid format | |
| } | |
| }, [customKernelNormalize, setCustomKernel, setCustomKernelSize]) | |
| const resetMatrix = useCallback(() => { | |
| const matrix = Array(customKernelSize).fill(0).map(() => Array(customKernelSize).fill(0)) | |
| const center = Math.floor(customKernelSize / 2) | |
| matrix[center][center] = 1 | |
| setCustomKernel(matrix, customKernelSize, customKernelNormalize) | |
| }, [customKernelSize, customKernelNormalize, setCustomKernel]) | |
| const validateMatrix = useCallback(() => { | |
| const sum = customKernelMatrix.flat().reduce((a, b) => a + b, 0) | |
| const hasNonZero = customKernelMatrix.flat().some(v => v !== 0) | |
| return { sum, hasNonZero, isNormalized: customKernelNormalize } | |
| }, [customKernelMatrix, customKernelNormalize]) | |
| const filteredKernels = PREDEFINED_KERNELS.filter( | |
| (k) => | |
| k.name.toLowerCase().includes(searchQuery.toLowerCase()) || | |
| k.category.toLowerCase().includes(searchQuery.toLowerCase()) || | |
| k.description.toLowerCase().includes(searchQuery.toLowerCase()) | |
| ) | |
| const kernelsByCategory = KERNEL_CATEGORIES.map((cat) => ({ | |
| ...cat, | |
| kernels: getKernelsByCategory(cat.value).filter((k) => | |
| k.name.toLowerCase().includes(searchQuery.toLowerCase()) | |
| ), | |
| })) | |
| return ( | |
| <Card className="w-full max-w-2xl"> | |
| <div className="flex items-center justify-between p-4 border-b dark:border-gray-700"> | |
| <h2 className="text-lg font-semibold">Kernel Selector</h2> | |
| <Button variant="ghost" size="icon" onClick={() => setShowCustom(!showCustom)}> | |
| <Plus className="h-4 w-4" /> | |
| </Button> | |
| </div> | |
| <CardContent className="p-4"> | |
| {/* Search */} | |
| <div className="relative mb-4"> | |
| <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" /> | |
| <input | |
| type="text" | |
| placeholder="Search kernels..." | |
| value={searchQuery} | |
| onChange={(e) => setSearchQuery(e.target.value)} | |
| className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg dark:border-gray-600 dark:bg-gray-800 dark:text-white" | |
| /> | |
| </div> | |
| {/* Category Tabs */} | |
| <div className="flex gap-1 mb-4 overflow-x-auto pb-2"> | |
| {KERNEL_CATEGORIES.map((cat) => ( | |
| <button | |
| key={cat.value} | |
| onClick={() => setActiveCategory(cat.value)} | |
| className={cn( | |
| 'px-3 py-1.5 text-sm rounded-full whitespace-nowrap transition-colors', | |
| activeCategory === cat.value | |
| ? 'bg-blue-600 text-white' | |
| : 'text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800' | |
| )} | |
| > | |
| {cat.label} | |
| </button> | |
| ))} | |
| </div> | |
| {/* Custom Kernel Editor */} | |
| {showCustom && ( | |
| <div className="mb-4 p-4 bg-gray-50 dark:bg-gray-800 rounded-lg border dark:border-gray-700"> | |
| <div className="flex items-center justify-between mb-3"> | |
| <h3 className="font-medium">Custom Kernel Editor</h3> | |
| <div className="flex items-center gap-2"> | |
| <Button variant="ghost" size="sm" onClick={copyMatrix} title="Copy matrix"> | |
| <Copy className="h-4 w-4" /> | |
| </Button> | |
| <Button variant="ghost" size="sm" onClick={pasteMatrix} title="Paste matrix"> | |
| <RotateCcw className="h-4 w-4" /> | |
| </Button> | |
| <Button variant="ghost" size="sm" onClick={resetMatrix} title="Reset to identity"> | |
| <RotateCcw className="h-4 w-4" /> | |
| </Button> | |
| </div> | |
| </div> | |
| <div className="flex items-center gap-4 mb-3 flex-wrap"> | |
| <Input | |
| type="number" | |
| label="Size" | |
| value={customKernelSize} | |
| onChange={(e) => setCustomKernelSize(Math.max(3, Math.min(9, parseInt(e.target.value) || 3)))} | |
| min={3} | |
| max={9} | |
| step={2} | |
| className="w-24" | |
| /> | |
| <label className="flex items-center gap-2 text-sm"> | |
| <input | |
| type="checkbox" | |
| checked={customKernelNormalize} | |
| onChange={(e) => setCustomKernelNormalize(e.target.checked)} | |
| className="rounded border-gray-300" | |
| /> | |
| Normalize | |
| </label> | |
| <div className="flex items-center gap-1 ml-auto text-xs text-gray-500"> | |
| {validateMatrix().hasNonZero ? ( | |
| <span className="flex items-center gap-1 text-green-600 dark:text-green-400"> | |
| <Check className="h-3 w-3" /> | |
| Valid | |
| </span> | |
| ) : ( | |
| <span className="text-red-600 dark:text-red-400">All zeros</span> | |
| )} | |
| {customKernelNormalize && ( | |
| <span className="text-blue-600 dark:text-blue-400">Σ={validateMatrix().sum.toFixed(2)}</span> | |
| )} | |
| </div> | |
| </div> | |
| {/* Preset Buttons */} | |
| <div className="mb-3 p-3 bg-gray-100 dark:bg-gray-700 rounded-lg"> | |
| <div className="flex items-center gap-2 mb-2"> | |
| <Sparkles className="h-4 w-4 text-gray-500" /> | |
| <span className="text-sm font-medium text-gray-700 dark:text-gray-300">Quick Presets:</span> | |
| </div> | |
| <div className="flex flex-wrap gap-2"> | |
| {Object.entries(CUSTOM_PRESETS).map(([key, preset]) => ( | |
| <Button | |
| key={key} | |
| variant={copiedPreset === key ? 'primary' : 'outline'} | |
| size="sm" | |
| onClick={() => applyPreset(key as keyof typeof CUSTOM_PRESETS)} | |
| className="text-xs" | |
| > | |
| {preset.name} | |
| </Button> | |
| ))} | |
| </div> | |
| </div> | |
| {/* Matrix Input Grid */} | |
| <div className="grid gap-1 mb-3" style={{ gridTemplateColumns: `repeat(${customKernelSize}, 1fr)` }}> | |
| {Array.from({ length: customKernelSize }, (_, row) => | |
| Array.from({ length: customKernelSize }, (_, col) => ( | |
| <input | |
| key={`${row}-${col}`} | |
| ref={(el) => { if (el && matrixInputRefs.current[row]) matrixInputRefs.current[row][col] = el }} | |
| type="number" | |
| step="0.1" | |
| value={customKernelMatrix[row]?.[col] ?? 0} | |
| onChange={(e) => updateCustomKernelCell(row, col, parseFloat(e.target.value) || 0)} | |
| onKeyDown={(e) => handleKeyDown(e, row, col)} | |
| className="w-full h-8 text-center text-sm border border-gray-300 rounded dark:border-gray-600 dark:bg-gray-700 focus:ring-2 focus:ring-blue-500 focus:border-transparent" | |
| title={`Row ${row + 1}, Col ${col + 1}`} | |
| /> | |
| )) | |
| )} | |
| </div> | |
| <div className="flex gap-2"> | |
| <Button variant="outline" onClick={copyMatrix} className="flex-1" size="sm"> | |
| <Copy className="h-4 w-4 mr-1" /> Copy | |
| </Button> | |
| <Button variant="outline" onClick={pasteMatrix} className="flex-1" size="sm"> | |
| <RotateCcw className="h-4 w-4 mr-1" /> Paste | |
| </Button> | |
| <Button onClick={handleCustomKernelSubmit} className="flex-1"> | |
| Apply Custom Kernel | |
| </Button> | |
| </div> | |
| </div> | |
| )} | |
| {/* Kernel List */} | |
| <div className="space-y-3 max-h-96 overflow-y-auto"> | |
| {kernelsByCategory | |
| .filter((cat) => cat.kernels.length > 0) | |
| .map((cat) => ( | |
| <div key={cat.value} className="space-y-2"> | |
| <h4 className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide"> | |
| {cat.label} | |
| </h4> | |
| <div className="grid gap-2" style={{ gridTemplateColumns: 'repeat(auto-fill, minmax(160px, 1fr))' }}> | |
| {cat.kernels.map((kernel) => ( | |
| <button | |
| key={kernel.id} | |
| onClick={() => handleKernelSelect(kernel)} | |
| className={cn( | |
| 'p-3 text-left rounded-lg border transition-all', | |
| 'hover:border-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20', | |
| selectedKernel?.id === kernel.id | |
| ? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20' | |
| : 'border-gray-200 dark:border-gray-700' | |
| )} | |
| > | |
| <div className="font-medium text-sm">{kernel.name}</div> | |
| <div className="text-xs text-gray-500 dark:text-gray-400 mt-1 line-clamp-2"> | |
| {kernel.description} | |
| </div> | |
| <div className="flex items-center gap-1 mt-2"> | |
| <Badge variant="outline" className="text-xs"> | |
| {kernel.size}×{kernel.size} | |
| </Badge> | |
| {kernel.normalize && ( | |
| <Badge variant="secondary" className="text-xs"> | |
| Normalized | |
| </Badge> | |
| )} | |
| </div> | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| ))} | |
| {filteredKernels.length === 0 && ( | |
| <div className="text-center py-8 text-gray-500"> | |
| No kernels found matching “{searchQuery}” | |
| </div> | |
| )} | |
| </div> | |
| </CardContent> | |
| </Card> | |
| ) | |
| } |