Update image classification UI and worker logic
Browse files- public/workers/image-classification.js +2 -4
- src/components/ModelInfo.tsx +3 -3
- src/components/ModelSelector.tsx +1 -1
- src/components/PipelineSelector.tsx +23 -67
- src/components/Sidebar.tsx +5 -10
- src/components/pipelines/ImageClassification.tsx +20 -39
- src/components/pipelines/ImageClassificationConfig.tsx +11 -10
public/workers/image-classification.js
CHANGED
|
@@ -62,7 +62,7 @@ class MyImageClassificationPipeline {
|
|
| 62 |
// Listen for messages from the main thread
|
| 63 |
self.addEventListener('message', async (event) => {
|
| 64 |
try {
|
| 65 |
-
const { type, image, model, dtype, topK =
|
| 66 |
|
| 67 |
if (!model) {
|
| 68 |
self.postMessage({
|
|
@@ -99,11 +99,9 @@ self.addEventListener('message', async (event) => {
|
|
| 99 |
}
|
| 100 |
|
| 101 |
try {
|
| 102 |
-
self.postMessage({ status: 'loading' })
|
| 103 |
-
|
| 104 |
// Run classification
|
| 105 |
const output = await classifier(image, {
|
| 106 |
-
|
| 107 |
})
|
| 108 |
|
| 109 |
// Format predictions
|
|
|
|
| 62 |
// Listen for messages from the main thread
|
| 63 |
self.addEventListener('message', async (event) => {
|
| 64 |
try {
|
| 65 |
+
const { type, image, model, dtype, topK = 1 } = event.data
|
| 66 |
|
| 67 |
if (!model) {
|
| 68 |
self.postMessage({
|
|
|
|
| 99 |
}
|
| 100 |
|
| 101 |
try {
|
|
|
|
|
|
|
| 102 |
// Run classification
|
| 103 |
const output = await classifier(image, {
|
| 104 |
+
top_k: topK
|
| 105 |
})
|
| 106 |
|
| 107 |
// Format predictions
|
src/components/ModelInfo.tsx
CHANGED
|
@@ -30,7 +30,7 @@ const ModelInfo = () => {
|
|
| 30 |
const ModelInfoSkeleton = () => (
|
| 31 |
<div className="bg-gradient-to-r from-secondary to-accent px-3 py-3 rounded-lg border border-border space-y-3 animate-pulse w-4/5">
|
| 32 |
<div className="flex items-center space-x-2">
|
| 33 |
-
<Bot className="w-4 h-4 text-
|
| 34 |
<div className="h-4 bg-muted rounded-sm flex-1"></div>
|
| 35 |
<div className="w-4 h-4 bg-muted rounded-full"></div>
|
| 36 |
</div>
|
|
@@ -45,7 +45,7 @@ const ModelInfo = () => {
|
|
| 45 |
<div className="h-3 bg-muted/80 rounded-sm w-8"></div>
|
| 46 |
</div>
|
| 47 |
<div className="flex items-center space-x-1">
|
| 48 |
-
<Download className="w-3 h-3 text-
|
| 49 |
<div className="h-3 bg-muted/80 rounded-sm w-8"></div>
|
| 50 |
</div>
|
| 51 |
<div className="flex items-center space-x-1">
|
|
@@ -53,7 +53,7 @@ const ModelInfo = () => {
|
|
| 53 |
<div className="h-3 bg-muted/80 rounded-sm w-8"></div>
|
| 54 |
</div>
|
| 55 |
<div className="flex items-center space-x-1">
|
| 56 |
-
<DatabaseIcon className="w-3 h-3 text-
|
| 57 |
<div className="h-3 bg-muted/80 rounded-sm w-12"></div>
|
| 58 |
</div>
|
| 59 |
</div>
|
|
|
|
| 30 |
const ModelInfoSkeleton = () => (
|
| 31 |
<div className="bg-gradient-to-r from-secondary to-accent px-3 py-3 rounded-lg border border-border space-y-3 animate-pulse w-4/5">
|
| 32 |
<div className="flex items-center space-x-2">
|
| 33 |
+
<Bot className="w-4 h-4 text-green-500" />
|
| 34 |
<div className="h-4 bg-muted rounded-sm flex-1"></div>
|
| 35 |
<div className="w-4 h-4 bg-muted rounded-full"></div>
|
| 36 |
</div>
|
|
|
|
| 45 |
<div className="h-3 bg-muted/80 rounded-sm w-8"></div>
|
| 46 |
</div>
|
| 47 |
<div className="flex items-center space-x-1">
|
| 48 |
+
<Download className="w-3 h-3 text-purple-500" />
|
| 49 |
<div className="h-3 bg-muted/80 rounded-sm w-8"></div>
|
| 50 |
</div>
|
| 51 |
<div className="flex items-center space-x-1">
|
|
|
|
| 53 |
<div className="h-3 bg-muted/80 rounded-sm w-8"></div>
|
| 54 |
</div>
|
| 55 |
<div className="flex items-center space-x-1">
|
| 56 |
+
<DatabaseIcon className="w-3 h-3 text-purple-500" />
|
| 57 |
<div className="h-3 bg-muted/80 rounded-sm w-12"></div>
|
| 58 |
</div>
|
| 59 |
</div>
|
src/components/ModelSelector.tsx
CHANGED
|
@@ -464,7 +464,7 @@ function ModelSelector() {
|
|
| 464 |
<div className="flex-1 min-w-0 pr-3">
|
| 465 |
<div className="flex items-center justify-between">
|
| 466 |
<Tooltip content={model.id}>
|
| 467 |
-
<span className="text-sm font-medium truncate block max-w-
|
| 468 |
{model.id}
|
| 469 |
</span>
|
| 470 |
</Tooltip>
|
|
|
|
| 464 |
<div className="flex-1 min-w-0 pr-3">
|
| 465 |
<div className="flex items-center justify-between">
|
| 466 |
<Tooltip content={model.id}>
|
| 467 |
+
<span className="text-sm font-medium truncate block max-w-[450px]">
|
| 468 |
{model.id}
|
| 469 |
</span>
|
| 470 |
</Tooltip>
|
src/components/PipelineSelector.tsx
CHANGED
|
@@ -1,18 +1,17 @@
|
|
| 1 |
import React from 'react'
|
| 2 |
import {
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
} from '
|
| 9 |
-
import { ChevronDown, Check } from 'lucide-react'
|
| 10 |
|
| 11 |
export const supportedPipelines = [
|
| 12 |
-
'text-generation',
|
| 13 |
'feature-extraction',
|
| 14 |
-
'zero-shot-classification',
|
| 15 |
'image-classification',
|
|
|
|
|
|
|
| 16 |
'text-classification',
|
| 17 |
'summarization',
|
| 18 |
'translation'
|
|
@@ -27,8 +26,6 @@ const PipelineSelector: React.FC<PipelineSelectorProps> = ({
|
|
| 27 |
pipeline,
|
| 28 |
setPipeline
|
| 29 |
}) => {
|
| 30 |
-
const selectedPipeline = pipeline
|
| 31 |
-
|
| 32 |
const formatPipelineName = (pipelineId: string) => {
|
| 33 |
return pipelineId
|
| 34 |
.split('-')
|
|
@@ -37,63 +34,22 @@ const PipelineSelector: React.FC<PipelineSelectorProps> = ({
|
|
| 37 |
}
|
| 38 |
|
| 39 |
return (
|
| 40 |
-
<
|
| 41 |
-
<
|
| 42 |
-
<
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
aria-hidden="true"
|
| 51 |
-
/>
|
| 52 |
-
</span>
|
| 53 |
-
</ListboxButton>
|
| 54 |
-
|
| 55 |
-
<Transition
|
| 56 |
-
enter="transition duration-100 ease-out"
|
| 57 |
-
enterFrom="transform scale-95 opacity-0"
|
| 58 |
-
enterTo="transform scale-100 opacity-100"
|
| 59 |
-
leave="transition duration-75 ease-out"
|
| 60 |
-
leaveFrom="transform scale-100 opacity-100"
|
| 61 |
-
leaveTo="transform scale-95 opacity-0"
|
| 62 |
>
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
`relative cursor-default select-none py-2 px-4 ${
|
| 69 |
-
active ? 'bg-amber-100 text-amber-900' : 'text-gray-900'
|
| 70 |
-
}`
|
| 71 |
-
}
|
| 72 |
-
value={p}
|
| 73 |
-
>
|
| 74 |
-
{({ selected }) => (
|
| 75 |
-
<div className="flex items-center justify-between">
|
| 76 |
-
<span
|
| 77 |
-
className={`block truncate ${
|
| 78 |
-
selected ? 'font-medium' : 'font-normal'
|
| 79 |
-
}`}
|
| 80 |
-
>
|
| 81 |
-
{formatPipelineName(p)}
|
| 82 |
-
</span>
|
| 83 |
-
{selected && (
|
| 84 |
-
<span className="flex items-center text-amber-600">
|
| 85 |
-
<Check className="h-5 w-5" aria-hidden="true" />
|
| 86 |
-
</span>
|
| 87 |
-
)}
|
| 88 |
-
</div>
|
| 89 |
-
)}
|
| 90 |
-
</ListboxOption>
|
| 91 |
-
))}
|
| 92 |
-
</ListboxOptions>
|
| 93 |
-
</Transition>
|
| 94 |
-
</div>
|
| 95 |
-
</Listbox>
|
| 96 |
-
</div>
|
| 97 |
)
|
| 98 |
}
|
| 99 |
|
|
|
|
| 1 |
import React from 'react'
|
| 2 |
import {
|
| 3 |
+
Select,
|
| 4 |
+
SelectContent,
|
| 5 |
+
SelectItem,
|
| 6 |
+
SelectTrigger,
|
| 7 |
+
SelectValue
|
| 8 |
+
} from '@/components/ui/select' // Adjust the import path as needed
|
|
|
|
| 9 |
|
| 10 |
export const supportedPipelines = [
|
|
|
|
| 11 |
'feature-extraction',
|
|
|
|
| 12 |
'image-classification',
|
| 13 |
+
'text-generation',
|
| 14 |
+
'zero-shot-classification',
|
| 15 |
'text-classification',
|
| 16 |
'summarization',
|
| 17 |
'translation'
|
|
|
|
| 26 |
pipeline,
|
| 27 |
setPipeline
|
| 28 |
}) => {
|
|
|
|
|
|
|
| 29 |
const formatPipelineName = (pipelineId: string) => {
|
| 30 |
return pipelineId
|
| 31 |
.split('-')
|
|
|
|
| 34 |
}
|
| 35 |
|
| 36 |
return (
|
| 37 |
+
<Select value={pipeline} onValueChange={setPipeline}>
|
| 38 |
+
<SelectTrigger className="w-full text-sm xl:text-base">
|
| 39 |
+
<SelectValue placeholder="Select a pipeline" />
|
| 40 |
+
</SelectTrigger>
|
| 41 |
+
<SelectContent>
|
| 42 |
+
{supportedPipelines.map((p) => (
|
| 43 |
+
<SelectItem
|
| 44 |
+
key={p}
|
| 45 |
+
value={p}
|
| 46 |
+
className="text-sm xl:text-base data-[state=checked]:font-bold"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
>
|
| 48 |
+
{formatPipelineName(p)}
|
| 49 |
+
</SelectItem>
|
| 50 |
+
))}
|
| 51 |
+
</SelectContent>
|
| 52 |
+
</Select>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
)
|
| 54 |
}
|
| 55 |
|
src/components/Sidebar.tsx
CHANGED
|
@@ -38,7 +38,7 @@ const Sidebar = ({ isOpen, onClose, setIsModalOpen }: SidebarProps) => {
|
|
| 38 |
<div className="flex flex-col h-full">
|
| 39 |
{/* Header */}
|
| 40 |
<div className="flex items-center justify-between p-4 border-b border-gray-200 lg:hidden">
|
| 41 |
-
<h2 className="text-lg font-semibold text-
|
| 42 |
Configuration
|
| 43 |
</h2>
|
| 44 |
<button
|
|
@@ -52,21 +52,16 @@ const Sidebar = ({ isOpen, onClose, setIsModalOpen }: SidebarProps) => {
|
|
| 52 |
{/* Content */}
|
| 53 |
<div className="flex-1 overflow-y-auto overflow-x-hidden p-4 space-y-6">
|
| 54 |
{/* Pipeline Selection */}
|
| 55 |
-
<div className="space-
|
| 56 |
-
<h3 className="text-md xl:text-lg font-semibold text-
|
| 57 |
Choose a Pipeline
|
| 58 |
</h3>
|
| 59 |
-
<
|
| 60 |
-
<PipelineSelector
|
| 61 |
-
pipeline={pipeline}
|
| 62 |
-
setPipeline={setPipeline}
|
| 63 |
-
/>
|
| 64 |
-
</div>
|
| 65 |
</div>
|
| 66 |
|
| 67 |
{/* Model Selection */}
|
| 68 |
<div className="space-y-3">
|
| 69 |
-
<h3 className="text-lg font-semibold text-
|
| 70 |
Select Model
|
| 71 |
</h3>
|
| 72 |
<ModelSelector />
|
|
|
|
| 38 |
<div className="flex flex-col h-full">
|
| 39 |
{/* Header */}
|
| 40 |
<div className="flex items-center justify-between p-4 border-b border-gray-200 lg:hidden">
|
| 41 |
+
<h2 className="text-lg font-semibold text-foreground">
|
| 42 |
Configuration
|
| 43 |
</h2>
|
| 44 |
<button
|
|
|
|
| 52 |
{/* Content */}
|
| 53 |
<div className="flex-1 overflow-y-auto overflow-x-hidden p-4 space-y-6">
|
| 54 |
{/* Pipeline Selection */}
|
| 55 |
+
<div className="space-x-3 flex flex-row justify-center align-center">
|
| 56 |
+
<h3 className="text-md xl:text-lg font-semibold text-foreground text-nowrap mt-1">
|
| 57 |
Choose a Pipeline
|
| 58 |
</h3>
|
| 59 |
+
<PipelineSelector pipeline={pipeline} setPipeline={setPipeline} />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
</div>
|
| 61 |
|
| 62 |
{/* Model Selection */}
|
| 63 |
<div className="space-y-3">
|
| 64 |
+
<h3 className="text-lg font-semibold text-foreground">
|
| 65 |
Select Model
|
| 66 |
</h3>
|
| 67 |
<ModelSelector />
|
src/components/pipelines/ImageClassification.tsx
CHANGED
|
@@ -49,7 +49,6 @@ function ImageClassification() {
|
|
| 49 |
} = useImageClassification()
|
| 50 |
|
| 51 |
const [isClassifying, setIsClassifying] = useState<boolean>(false)
|
| 52 |
-
const [showPreviews, setShowPreviews] = useState<boolean>(true)
|
| 53 |
const [dragOver, setDragOver] = useState<boolean>(false)
|
| 54 |
const [progress, setProgress] = useState<number | null>(null)
|
| 55 |
|
|
@@ -71,7 +70,6 @@ function ImageClassification() {
|
|
| 71 |
updateExample(example.id, { isLoading: true })
|
| 72 |
setIsClassifying(true)
|
| 73 |
setProgress(0)
|
| 74 |
-
|
| 75 |
const message: ImageClassificationWorkerInput = {
|
| 76 |
type: 'classify',
|
| 77 |
image: example.url,
|
|
@@ -135,7 +133,9 @@ function ImageClassification() {
|
|
| 135 |
)
|
| 136 |
|
| 137 |
const handleLoadSampleImages = useCallback(async () => {
|
|
|
|
| 138 |
for (const sample of SAMPLE_IMAGES) {
|
|
|
|
| 139 |
try {
|
| 140 |
const response = await fetch(sample.url)
|
| 141 |
const blob = await response.blob()
|
|
@@ -145,14 +145,13 @@ function ImageClassification() {
|
|
| 145 |
console.error(`Failed to load sample image ${sample.name}:`, error)
|
| 146 |
}
|
| 147 |
}
|
| 148 |
-
}, [addExample])
|
| 149 |
|
| 150 |
useEffect(() => {
|
| 151 |
if (!activeWorker) return
|
| 152 |
|
| 153 |
const onMessageReceived = (e: MessageEvent<WorkerMessage>) => {
|
| 154 |
const { status, output, progress: workerProgress } = e.data
|
| 155 |
-
|
| 156 |
if (status === 'progress' && workerProgress !== undefined) {
|
| 157 |
setProgress(workerProgress)
|
| 158 |
} else if (status === 'output' && output?.predictions) {
|
|
@@ -197,17 +196,6 @@ function ImageClassification() {
|
|
| 197 |
>
|
| 198 |
Load Samples
|
| 199 |
</button>
|
| 200 |
-
<button
|
| 201 |
-
onClick={() => setShowPreviews(!showPreviews)}
|
| 202 |
-
className="p-2 bg-blue-100 hover:bg-blue-200 rounded-lg transition-colors"
|
| 203 |
-
title={showPreviews ? 'Hide Previews' : 'Show Previews'}
|
| 204 |
-
>
|
| 205 |
-
{showPreviews ? (
|
| 206 |
-
<EyeOff className="w-4 h-4" />
|
| 207 |
-
) : (
|
| 208 |
-
<Eye className="w-4 h-4" />
|
| 209 |
-
)}
|
| 210 |
-
</button>
|
| 211 |
<button
|
| 212 |
onClick={clearExamples}
|
| 213 |
className="p-2 bg-red-100 hover:bg-red-200 rounded-lg transition-colors"
|
|
@@ -218,7 +206,7 @@ function ImageClassification() {
|
|
| 218 |
</div>
|
| 219 |
</div>
|
| 220 |
|
| 221 |
-
<div className="flex flex-col lg:flex-row gap-4 flex-1">
|
| 222 |
{/* Left Panel - Image Upload and List */}
|
| 223 |
<div className="lg:w-1/2 flex flex-col">
|
| 224 |
{/* Upload Area */}
|
|
@@ -281,9 +269,9 @@ function ImageClassification() {
|
|
| 281 |
)}
|
| 282 |
|
| 283 |
{/* Images List */}
|
| 284 |
-
<div className="flex-1 overflow-y-auto border border-gray-300 rounded-lg bg-white">
|
| 285 |
<div className="p-4">
|
| 286 |
-
<h3 className="text-sm font-medium text-gray-700 mb-3">
|
| 287 |
Images ({examples.length})
|
| 288 |
</h3>
|
| 289 |
{examples.length === 0 ? (
|
|
@@ -292,7 +280,7 @@ function ImageClassification() {
|
|
| 292 |
started.
|
| 293 |
</div>
|
| 294 |
) : (
|
| 295 |
-
<div className="
|
| 296 |
{examples.map((example) => (
|
| 297 |
<div
|
| 298 |
key={example.id}
|
|
@@ -304,15 +292,13 @@ function ImageClassification() {
|
|
| 304 |
onClick={() => handleSelectExample(example)}
|
| 305 |
>
|
| 306 |
<div className="flex gap-3">
|
| 307 |
-
|
| 308 |
-
<
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
</div>
|
| 315 |
-
)}
|
| 316 |
<div className="flex-1 min-w-0">
|
| 317 |
<div className="flex justify-between items-start">
|
| 318 |
<div className="flex-1 min-w-0">
|
|
@@ -334,11 +320,6 @@ function ImageClassification() {
|
|
| 334 |
Not classified
|
| 335 |
</div>
|
| 336 |
)}
|
| 337 |
-
{selectedExample?.id === example.id && (
|
| 338 |
-
<div className="text-xs text-blue-600">
|
| 339 |
-
Selected
|
| 340 |
-
</div>
|
| 341 |
-
)}
|
| 342 |
</div>
|
| 343 |
</div>
|
| 344 |
<button
|
|
@@ -362,19 +343,19 @@ function ImageClassification() {
|
|
| 362 |
</div>
|
| 363 |
|
| 364 |
{/* Right Panel - Preview and Results */}
|
| 365 |
-
<div className="lg:w-1/2 flex flex-col">
|
| 366 |
{/* Image Preview */}
|
| 367 |
{selectedExample && (
|
| 368 |
<div className="mb-4">
|
| 369 |
<h3 className="text-sm font-medium text-gray-700 mb-2">
|
| 370 |
Selected Image
|
| 371 |
</h3>
|
| 372 |
-
<div className="border border-gray-300 rounded-lg bg-white p-4">
|
| 373 |
<div className="flex flex-col items-center">
|
| 374 |
<img
|
| 375 |
src={selectedExample.url}
|
| 376 |
alt={selectedExample.name}
|
| 377 |
-
className="max-w-full max-h-64 object-contain rounded-lg mb-2"
|
| 378 |
/>
|
| 379 |
<div className="text-sm text-gray-600 text-center">
|
| 380 |
{selectedExample.name}
|
|
@@ -385,9 +366,9 @@ function ImageClassification() {
|
|
| 385 |
)}
|
| 386 |
|
| 387 |
{/* Classification Results */}
|
| 388 |
-
<div className="flex-1 overflow-y-auto border border-gray-300 rounded-lg bg-white">
|
| 389 |
<div className="p-4">
|
| 390 |
-
<h3 className="text-sm font-medium text-gray-700 mb-3">
|
| 391 |
Classification Results
|
| 392 |
{selectedExample && ` - ${selectedExample.name}`}
|
| 393 |
</h3>
|
|
@@ -419,7 +400,7 @@ function ImageClassification() {
|
|
| 419 |
</button>
|
| 420 |
</div>
|
| 421 |
) : (
|
| 422 |
-
<div className="space-y-3">
|
| 423 |
{selectedExample.predictions.map((prediction, index) => {
|
| 424 |
const confidencePercent = (prediction.score * 100).toFixed(
|
| 425 |
1
|
|
|
|
| 49 |
} = useImageClassification()
|
| 50 |
|
| 51 |
const [isClassifying, setIsClassifying] = useState<boolean>(false)
|
|
|
|
| 52 |
const [dragOver, setDragOver] = useState<boolean>(false)
|
| 53 |
const [progress, setProgress] = useState<number | null>(null)
|
| 54 |
|
|
|
|
| 70 |
updateExample(example.id, { isLoading: true })
|
| 71 |
setIsClassifying(true)
|
| 72 |
setProgress(0)
|
|
|
|
| 73 |
const message: ImageClassificationWorkerInput = {
|
| 74 |
type: 'classify',
|
| 75 |
image: example.url,
|
|
|
|
| 133 |
)
|
| 134 |
|
| 135 |
const handleLoadSampleImages = useCallback(async () => {
|
| 136 |
+
const existstingImages = new Set(examples.map((ex) => ex.name))
|
| 137 |
for (const sample of SAMPLE_IMAGES) {
|
| 138 |
+
if (existstingImages.has(sample.name)) continue
|
| 139 |
try {
|
| 140 |
const response = await fetch(sample.url)
|
| 141 |
const blob = await response.blob()
|
|
|
|
| 145 |
console.error(`Failed to load sample image ${sample.name}:`, error)
|
| 146 |
}
|
| 147 |
}
|
| 148 |
+
}, [addExample, examples])
|
| 149 |
|
| 150 |
useEffect(() => {
|
| 151 |
if (!activeWorker) return
|
| 152 |
|
| 153 |
const onMessageReceived = (e: MessageEvent<WorkerMessage>) => {
|
| 154 |
const { status, output, progress: workerProgress } = e.data
|
|
|
|
| 155 |
if (status === 'progress' && workerProgress !== undefined) {
|
| 156 |
setProgress(workerProgress)
|
| 157 |
} else if (status === 'output' && output?.predictions) {
|
|
|
|
| 196 |
>
|
| 197 |
Load Samples
|
| 198 |
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
<button
|
| 200 |
onClick={clearExamples}
|
| 201 |
className="p-2 bg-red-100 hover:bg-red-200 rounded-lg transition-colors"
|
|
|
|
| 206 |
</div>
|
| 207 |
</div>
|
| 208 |
|
| 209 |
+
<div className="flex flex-col lg:flex-row gap-4 flex-1 min-h-0">
|
| 210 |
{/* Left Panel - Image Upload and List */}
|
| 211 |
<div className="lg:w-1/2 flex flex-col">
|
| 212 |
{/* Upload Area */}
|
|
|
|
| 269 |
)}
|
| 270 |
|
| 271 |
{/* Images List */}
|
| 272 |
+
<div className="flex-1 overflow-y-auto border border-gray-300 rounded-lg bg-white min-h-0 max-h-[30vh] sm:max-h-[20vh] lg:max-h-none">
|
| 273 |
<div className="p-4">
|
| 274 |
+
<h3 className="text-sm font-medium text-gray-700 mb-3 sticky top-0 bg-white z-10">
|
| 275 |
Images ({examples.length})
|
| 276 |
</h3>
|
| 277 |
{examples.length === 0 ? (
|
|
|
|
| 280 |
started.
|
| 281 |
</div>
|
| 282 |
) : (
|
| 283 |
+
<div className="overflow-y-auto max-h-[calc(100%-10rem)] grid-cols-2 grid sm:grid-cols-3 lg:grid-cols-1 gap-2 ">
|
| 284 |
{examples.map((example) => (
|
| 285 |
<div
|
| 286 |
key={example.id}
|
|
|
|
| 292 |
onClick={() => handleSelectExample(example)}
|
| 293 |
>
|
| 294 |
<div className="flex gap-3">
|
| 295 |
+
<div className="shrink-0">
|
| 296 |
+
<img
|
| 297 |
+
src={example.url}
|
| 298 |
+
alt={example.name}
|
| 299 |
+
className="w-16 h-16 object-cover rounded-lg"
|
| 300 |
+
/>
|
| 301 |
+
</div>
|
|
|
|
|
|
|
| 302 |
<div className="flex-1 min-w-0">
|
| 303 |
<div className="flex justify-between items-start">
|
| 304 |
<div className="flex-1 min-w-0">
|
|
|
|
| 320 |
Not classified
|
| 321 |
</div>
|
| 322 |
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 323 |
</div>
|
| 324 |
</div>
|
| 325 |
<button
|
|
|
|
| 343 |
</div>
|
| 344 |
|
| 345 |
{/* Right Panel - Preview and Results */}
|
| 346 |
+
<div className="lg:w-1/2 flex lg:flex-col flex-row space-x-4 sm:max-h-[34vh] lg:max-h-none">
|
| 347 |
{/* Image Preview */}
|
| 348 |
{selectedExample && (
|
| 349 |
<div className="mb-4">
|
| 350 |
<h3 className="text-sm font-medium text-gray-700 mb-2">
|
| 351 |
Selected Image
|
| 352 |
</h3>
|
| 353 |
+
<div className="sm:border-none border border-gray-300 rounded-lg bg-white p-4 sm:p-0">
|
| 354 |
<div className="flex flex-col items-center">
|
| 355 |
<img
|
| 356 |
src={selectedExample.url}
|
| 357 |
alt={selectedExample.name}
|
| 358 |
+
className="max-w-64 lg:max-w-full max-h-60 lg:max-h-64 object-contain rounded-lg mb-2"
|
| 359 |
/>
|
| 360 |
<div className="text-sm text-gray-600 text-center">
|
| 361 |
{selectedExample.name}
|
|
|
|
| 366 |
)}
|
| 367 |
|
| 368 |
{/* Classification Results */}
|
| 369 |
+
<div className="flex-1 overflow-y-auto border border-gray-300 rounded-lg bg-white ">
|
| 370 |
<div className="p-4">
|
| 371 |
+
<h3 className="text-sm font-medium text-gray-700 mb-3 sticky top-0 bg-white">
|
| 372 |
Classification Results
|
| 373 |
{selectedExample && ` - ${selectedExample.name}`}
|
| 374 |
</h3>
|
|
|
|
| 400 |
</button>
|
| 401 |
</div>
|
| 402 |
) : (
|
| 403 |
+
<div className="space-y-3 overflow-y-auto max-h-[calc(100%-3rem)]">
|
| 404 |
{selectedExample.predictions.map((prediction, index) => {
|
| 405 |
const confidencePercent = (prediction.score * 100).toFixed(
|
| 406 |
1
|
src/components/pipelines/ImageClassificationConfig.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
import React from 'react'
|
| 2 |
import { useImageClassification } from '../../contexts/ImageClassificationContext'
|
|
|
|
| 3 |
|
| 4 |
const ImageClassificationConfig = () => {
|
| 5 |
const { topK, setTopK } = useImageClassification()
|
|
@@ -15,18 +16,18 @@ const ImageClassificationConfig = () => {
|
|
| 15 |
<label className="block text-sm font-medium text-foreground/80 mb-1">
|
| 16 |
Top K Predictions: {topK}
|
| 17 |
</label>
|
| 18 |
-
<
|
| 19 |
-
|
| 20 |
-
min=
|
| 21 |
-
max=
|
| 22 |
-
step=
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
className="w-full h-2 bg-muted rounded-lg appearance-none cursor-pointer"
|
| 26 |
/>
|
| 27 |
<div className="flex justify-between text-xs text-muted-foreground/60 mt-1">
|
| 28 |
<span>1</span>
|
| 29 |
-
<span>
|
|
|
|
| 30 |
<span>10</span>
|
| 31 |
</div>
|
| 32 |
<p className="text-xs text-muted-foreground mt-1">
|
|
@@ -36,7 +37,7 @@ const ImageClassificationConfig = () => {
|
|
| 36 |
|
| 37 |
<div className="p-3 bg-chart-4/10 border border-chart-4/20 rounded-lg">
|
| 38 |
<h4 className="text-sm font-medium text-chart-4 mb-2">💡 Tips</h4>
|
| 39 |
-
<div className="text-xs text-chart-4
|
| 40 |
<p>• Use Top K = 3-5 for most cases</p>
|
| 41 |
<p>• Smaller images process faster</p>
|
| 42 |
<p>• Try quantized models for speed</p>
|
|
|
|
| 1 |
import React from 'react'
|
| 2 |
import { useImageClassification } from '../../contexts/ImageClassificationContext'
|
| 3 |
+
import { Slider } from '../ui/slider'
|
| 4 |
|
| 5 |
const ImageClassificationConfig = () => {
|
| 6 |
const { topK, setTopK } = useImageClassification()
|
|
|
|
| 16 |
<label className="block text-sm font-medium text-foreground/80 mb-1">
|
| 17 |
Top K Predictions: {topK}
|
| 18 |
</label>
|
| 19 |
+
<Slider
|
| 20 |
+
defaultValue={[topK]}
|
| 21 |
+
min={1}
|
| 22 |
+
max={10}
|
| 23 |
+
step={1}
|
| 24 |
+
onValueChange={(value) => setTopK(value[0])}
|
| 25 |
+
className="w-full rounded-lg"
|
|
|
|
| 26 |
/>
|
| 27 |
<div className="flex justify-between text-xs text-muted-foreground/60 mt-1">
|
| 28 |
<span>1</span>
|
| 29 |
+
<span>4</span>
|
| 30 |
+
<span>7</span>
|
| 31 |
<span>10</span>
|
| 32 |
</div>
|
| 33 |
<p className="text-xs text-muted-foreground mt-1">
|
|
|
|
| 37 |
|
| 38 |
<div className="p-3 bg-chart-4/10 border border-chart-4/20 rounded-lg">
|
| 39 |
<h4 className="text-sm font-medium text-chart-4 mb-2">💡 Tips</h4>
|
| 40 |
+
<div className="text-xs text-chart-4 space-y-1">
|
| 41 |
<p>• Use Top K = 3-5 for most cases</p>
|
| 42 |
<p>• Smaller images process faster</p>
|
| 43 |
<p>• Try quantized models for speed</p>
|