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>
|