mihirmithani
Add Kernel Convolution Visualizer - Phase 1 complete
80bcac8
Raw History Blame
7.65 kB
'use client'
import { useState } from 'react'
import { useAppStore } from '@/store'
import { PREDEFINED_KERNELS, KERNEL_CATEGORIES, getKernelsByCategory } 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 } from 'lucide-react'
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 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())
),
}))
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)
}
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">
<h3 className="font-medium mb-3">Custom Kernel Editor</h3>
<div className="flex items-center gap-4 mb-3">
<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>
<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}`}
type="number"
step="0.1"
value={customKernelMatrix[row]?.[col] ?? 0}
onChange={(e) => updateCustomKernelCell(row, col, parseFloat(e.target.value) || 0)}
className="w-full h-8 text-center text-sm border border-gray-300 rounded dark:border-gray-600 dark:bg-gray-700"
/>
))
)}
</div>
<Button onClick={handleCustomKernelSubmit} className="w-full">
Apply Custom Kernel
</Button>
</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 &ldquo;{searchQuery}&rdquo;
</div>
)}
</div>
</CardContent>
</Card>
)
}