Prevent example duplicates and improve responsive UI in
Browse files
src/components/pipelines/FeatureExtraction.tsx
CHANGED
@@ -142,9 +142,21 @@ function FeatureExtraction() {
|
|
142 |
const handleAddExample = useCallback(() => {
|
143 |
if (!newExampleText.trim()) return
|
144 |
|
145 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
146 |
setNewExampleText('')
|
147 |
-
}, [newExampleText, addExample])
|
148 |
|
149 |
const handleExtractAll = useCallback(() => {
|
150 |
const textsToExtract = examples
|
@@ -167,8 +179,13 @@ function FeatureExtraction() {
|
|
167 |
)
|
168 |
|
169 |
const handleLoadSampleData = useCallback(() => {
|
170 |
-
|
171 |
-
|
|
|
|
|
|
|
|
|
|
|
172 |
|
173 |
useEffect(() => {
|
174 |
if (!activeWorker) return
|
@@ -224,14 +241,16 @@ function FeatureExtraction() {
|
|
224 |
const busy = status !== 'ready' || isExtracting
|
225 |
|
226 |
return (
|
227 |
-
<div className="flex flex-col h-full max-h-[
|
228 |
-
<div className="flex items-center justify-between mb-4">
|
229 |
-
<h1 className="text-2xl font-bold">
|
230 |
-
|
|
|
|
|
231 |
<button
|
232 |
onClick={handleLoadSampleData}
|
233 |
disabled={!hasBeenLoaded || isExtracting}
|
234 |
-
className="px-3 py-2 bg-purple-100 hover:bg-purple-200 disabled:bg-gray-100 disabled:cursor-not-allowed rounded-lg transition-colors text-sm"
|
235 |
title="Load Sample Data"
|
236 |
>
|
237 |
Load Samples
|
@@ -259,28 +278,28 @@ function FeatureExtraction() {
|
|
259 |
</div>
|
260 |
</div>
|
261 |
|
262 |
-
<div className="flex flex-col lg:flex-row gap-4 flex-1">
|
263 |
{/* Left Panel - Examples */}
|
264 |
-
<div className="lg:w-1/2 flex flex-col">
|
265 |
{/* Add Example */}
|
266 |
<div className="mb-4">
|
267 |
<label className="block text-sm font-medium text-gray-700 mb-2">
|
268 |
Add Text Examples:
|
269 |
</label>
|
270 |
-
<div className="flex gap-2">
|
271 |
<textarea
|
272 |
value={newExampleText}
|
273 |
onChange={(e) => setNewExampleText(e.target.value)}
|
274 |
onKeyPress={handleKeyPress}
|
275 |
placeholder="Enter text to get embeddings... (Press Enter to add)"
|
276 |
-
className="flex-1 p-3 border border-gray-300 rounded-lg resize-none focus:outline-hidden focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed"
|
277 |
rows={2}
|
278 |
disabled={!hasBeenLoaded || isExtracting}
|
279 |
/>
|
280 |
<button
|
281 |
onClick={handleAddExample}
|
282 |
disabled={!newExampleText.trim() || !hasBeenLoaded}
|
283 |
-
className="px-4 py-2 bg-blue-500 hover:bg-blue-600 disabled:bg-gray-300 disabled:cursor-not-allowed text-white rounded-lg transition-colors"
|
284 |
>
|
285 |
<Plus className="w-4 h-4" />
|
286 |
</button>
|
@@ -309,9 +328,9 @@ function FeatureExtraction() {
|
|
309 |
)}
|
310 |
|
311 |
{/* Examples List */}
|
312 |
-
<div className="flex-1 overflow-y-auto border border-gray-300 rounded-lg bg-white">
|
313 |
-
<div className="p-4">
|
314 |
-
<h3 className="text-sm font-medium text-gray-700 mb-3">
|
315 |
Examples ({examples.length})
|
316 |
</h3>
|
317 |
{examples.length === 0 ? (
|
@@ -319,11 +338,11 @@ function FeatureExtraction() {
|
|
319 |
No examples added yet. Add some text above to get started.
|
320 |
</div>
|
321 |
) : (
|
322 |
-
<div className="space-y-2">
|
323 |
{examples.map((example) => (
|
324 |
<div
|
325 |
key={example.id}
|
326 |
-
className={`p-3 border rounded-lg cursor-pointer transition-colors ${
|
327 |
selectedExample?.id === example.id
|
328 |
? 'border-blue-500 bg-blue-50'
|
329 |
: 'border-gray-200 hover:border-gray-300'
|
@@ -377,19 +396,19 @@ function FeatureExtraction() {
|
|
377 |
</div>
|
378 |
|
379 |
{/* Right Panel - Visualization and Similarities */}
|
380 |
-
<div className="lg:w-1/2 flex flex-col">
|
381 |
{showVisualization && (
|
382 |
<div className="mb-4">
|
383 |
<h3 className="text-sm font-medium text-gray-700 mb-2">
|
384 |
2D Visualization
|
385 |
</h3>
|
386 |
-
<div className="border border-gray-300 rounded-lg bg-white p-4">
|
387 |
<svg
|
388 |
ref={chartRef}
|
389 |
width="100%"
|
390 |
-
height="
|
391 |
viewBox="0 0 400 300"
|
392 |
-
className="border border-gray-100"
|
393 |
>
|
394 |
{points2D.map((point) => {
|
395 |
const isSelected = selectedExample?.id === point.id
|
@@ -463,30 +482,30 @@ function FeatureExtraction() {
|
|
463 |
</div>
|
464 |
)}
|
465 |
{points2D.length > 0 && (
|
466 |
-
<div className="mt-3 p-3 bg-gray-50 rounded-lg">
|
467 |
<h4 className="text-xs font-medium text-gray-700 mb-2">
|
468 |
Legend:
|
469 |
</h4>
|
470 |
-
<div className="flex flex-wrap gap-3 text-xs">
|
471 |
<div className="flex items-center gap-1">
|
472 |
-
<div className="w-3 h-3 rounded-full bg-blue-500"></div>
|
473 |
-
<span>Selected</span>
|
474 |
</div>
|
475 |
<div className="flex items-center gap-1">
|
476 |
-
<div className="w-3 h-3 rounded-full bg-green-500"></div>
|
477 |
-
<span>High
|
478 |
</div>
|
479 |
<div className="flex items-center gap-1">
|
480 |
-
<div className="w-3 h-3 rounded-full bg-yellow-500"></div>
|
481 |
-
<span>
|
482 |
</div>
|
483 |
<div className="flex items-center gap-1">
|
484 |
-
<div className="w-3 h-3 rounded-full bg-red-500"></div>
|
485 |
-
<span>Low
|
486 |
</div>
|
487 |
<div className="flex items-center gap-1">
|
488 |
-
<div className="w-3 h-3 rounded-full bg-gray-500"></div>
|
489 |
-
<span>Not compared</span>
|
490 |
</div>
|
491 |
</div>
|
492 |
</div>
|
@@ -496,9 +515,9 @@ function FeatureExtraction() {
|
|
496 |
)}
|
497 |
|
498 |
{/* Similarity Results */}
|
499 |
-
<div className="flex-1 overflow-y-auto border border-gray-300 rounded-lg bg-white">
|
500 |
-
<div className="p-4">
|
501 |
-
<h3 className="text-sm font-medium text-gray-700 mb-3">
|
502 |
Cosine Similarities
|
503 |
{selectedExample &&
|
504 |
` (vs "${selectedExample.text.substring(0, 30)}...")`}
|
@@ -512,7 +531,7 @@ function FeatureExtraction() {
|
|
512 |
No other examples with embeddings to compare
|
513 |
</div>
|
514 |
) : (
|
515 |
-
<div className="space-y-2">
|
516 |
{similarities.map((sim) => {
|
517 |
const example = examples.find(
|
518 |
(ex) => ex.id === sim.exampleId
|
@@ -530,7 +549,7 @@ function FeatureExtraction() {
|
|
530 |
return (
|
531 |
<div
|
532 |
key={sim.exampleId}
|
533 |
-
className="p-3 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors"
|
534 |
>
|
535 |
<div className="flex justify-between items-start">
|
536 |
<div className="flex-1 min-w-0">
|
|
|
142 |
const handleAddExample = useCallback(() => {
|
143 |
if (!newExampleText.trim()) return
|
144 |
|
145 |
+
// Check for duplicates
|
146 |
+
const trimmedText = newExampleText.trim()
|
147 |
+
const isDuplicate = examples.some(
|
148 |
+
(example) => example.text.toLowerCase() === trimmedText.toLowerCase()
|
149 |
+
)
|
150 |
+
|
151 |
+
if (isDuplicate) {
|
152 |
+
// Optionally show a toast or alert here
|
153 |
+
setNewExampleText('')
|
154 |
+
return
|
155 |
+
}
|
156 |
+
|
157 |
+
addExample(trimmedText)
|
158 |
setNewExampleText('')
|
159 |
+
}, [newExampleText, addExample, examples])
|
160 |
|
161 |
const handleExtractAll = useCallback(() => {
|
162 |
const textsToExtract = examples
|
|
|
179 |
)
|
180 |
|
181 |
const handleLoadSampleData = useCallback(() => {
|
182 |
+
const existingTexts = new Set(examples.map((ex) => ex.text.toLowerCase()))
|
183 |
+
SAMPLE_TEXTS.forEach((text) => {
|
184 |
+
if (!existingTexts.has(text.toLowerCase())) {
|
185 |
+
addExample(text)
|
186 |
+
}
|
187 |
+
})
|
188 |
+
}, [addExample, examples])
|
189 |
|
190 |
useEffect(() => {
|
191 |
if (!activeWorker) return
|
|
|
241 |
const busy = status !== 'ready' || isExtracting
|
242 |
|
243 |
return (
|
244 |
+
<div className="flex flex-col h-full max-h-[88vh] w-full p-4">
|
245 |
+
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-4 gap-2">
|
246 |
+
<h1 className="text-xl sm:text-2xl font-bold">
|
247 |
+
Feature Extraction (Embeddings)
|
248 |
+
</h1>
|
249 |
+
<div className="flex flex-nowrap gap-2">
|
250 |
<button
|
251 |
onClick={handleLoadSampleData}
|
252 |
disabled={!hasBeenLoaded || isExtracting}
|
253 |
+
className="px-3 py-2 bg-purple-100 hover:bg-purple-200 disabled:bg-gray-100 disabled:cursor-not-allowed rounded-lg transition-colors text-xs sm:text-sm"
|
254 |
title="Load Sample Data"
|
255 |
>
|
256 |
Load Samples
|
|
|
278 |
</div>
|
279 |
</div>
|
280 |
|
281 |
+
<div className="flex flex-col lg:flex-row gap-4 flex-1 min-h-0">
|
282 |
{/* Left Panel - Examples */}
|
283 |
+
<div className="lg:w-1/2 flex flex-col min-h-0">
|
284 |
{/* Add Example */}
|
285 |
<div className="mb-4">
|
286 |
<label className="block text-sm font-medium text-gray-700 mb-2">
|
287 |
Add Text Examples:
|
288 |
</label>
|
289 |
+
<div className="flex flex-col sm:flex-row gap-2">
|
290 |
<textarea
|
291 |
value={newExampleText}
|
292 |
onChange={(e) => setNewExampleText(e.target.value)}
|
293 |
onKeyPress={handleKeyPress}
|
294 |
placeholder="Enter text to get embeddings... (Press Enter to add)"
|
295 |
+
className="flex-1 p-3 border border-gray-300 rounded-lg resize-none focus:outline-hidden focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed text-sm"
|
296 |
rows={2}
|
297 |
disabled={!hasBeenLoaded || isExtracting}
|
298 |
/>
|
299 |
<button
|
300 |
onClick={handleAddExample}
|
301 |
disabled={!newExampleText.trim() || !hasBeenLoaded}
|
302 |
+
className="px-4 py-2 bg-blue-500 hover:bg-blue-600 disabled:bg-gray-300 disabled:cursor-not-allowed text-white rounded-lg transition-colors self-start sm:self-stretch"
|
303 |
>
|
304 |
<Plus className="w-4 h-4" />
|
305 |
</button>
|
|
|
328 |
)}
|
329 |
|
330 |
{/* Examples List */}
|
331 |
+
<div className="flex-1 overflow-y-auto border border-gray-300 rounded-lg bg-white min-h-0 max-h-[35vh] sm:max-h-[40vh] lg:max-h-none">
|
332 |
+
<div className="p-4 h-full">
|
333 |
+
<h3 className="text-sm font-medium text-gray-700 mb-3 sticky top-0 bg-white z-10">
|
334 |
Examples ({examples.length})
|
335 |
</h3>
|
336 |
{examples.length === 0 ? (
|
|
|
338 |
No examples added yet. Add some text above to get started.
|
339 |
</div>
|
340 |
) : (
|
341 |
+
<div className="space-y-2 overflow-y-auto max-h-[calc(100%-3rem)]">
|
342 |
{examples.map((example) => (
|
343 |
<div
|
344 |
key={example.id}
|
345 |
+
className={`p-2 sm:p-3 border rounded-lg cursor-pointer transition-colors ${
|
346 |
selectedExample?.id === example.id
|
347 |
? 'border-blue-500 bg-blue-50'
|
348 |
: 'border-gray-200 hover:border-gray-300'
|
|
|
396 |
</div>
|
397 |
|
398 |
{/* Right Panel - Visualization and Similarities */}
|
399 |
+
<div className="lg:w-1/2 flex flex-col min-h-0">
|
400 |
{showVisualization && (
|
401 |
<div className="mb-4">
|
402 |
<h3 className="text-sm font-medium text-gray-700 mb-2">
|
403 |
2D Visualization
|
404 |
</h3>
|
405 |
+
<div className="border border-gray-300 rounded-lg bg-white p-2 sm:p-4">
|
406 |
<svg
|
407 |
ref={chartRef}
|
408 |
width="100%"
|
409 |
+
height="250"
|
410 |
viewBox="0 0 400 300"
|
411 |
+
className="border border-gray-100 sm:h-[300px]"
|
412 |
>
|
413 |
{points2D.map((point) => {
|
414 |
const isSelected = selectedExample?.id === point.id
|
|
|
482 |
</div>
|
483 |
)}
|
484 |
{points2D.length > 0 && (
|
485 |
+
<div className="mt-3 p-2 sm:p-3 bg-gray-50 rounded-lg">
|
486 |
<h4 className="text-xs font-medium text-gray-700 mb-2">
|
487 |
Legend:
|
488 |
</h4>
|
489 |
+
<div className="flex flex-wrap gap-2 sm:gap-3 text-xs">
|
490 |
<div className="flex items-center gap-1">
|
491 |
+
<div className="w-2 h-2 sm:w-3 sm:h-3 rounded-full bg-blue-500"></div>
|
492 |
+
<span className="text-xs">Selected</span>
|
493 |
</div>
|
494 |
<div className="flex items-center gap-1">
|
495 |
+
<div className="w-2 h-2 sm:w-3 sm:h-3 rounded-full bg-green-500"></div>
|
496 |
+
<span className="text-xs">High (>80%)</span>
|
497 |
</div>
|
498 |
<div className="flex items-center gap-1">
|
499 |
+
<div className="w-2 h-2 sm:w-3 sm:h-3 rounded-full bg-yellow-500"></div>
|
500 |
+
<span className="text-xs">Med (50-80%)</span>
|
501 |
</div>
|
502 |
<div className="flex items-center gap-1">
|
503 |
+
<div className="w-2 h-2 sm:w-3 sm:h-3 rounded-full bg-red-500"></div>
|
504 |
+
<span className="text-xs">Low (<50%)</span>
|
505 |
</div>
|
506 |
<div className="flex items-center gap-1">
|
507 |
+
<div className="w-2 h-2 sm:w-3 sm:h-3 rounded-full bg-gray-500"></div>
|
508 |
+
<span className="text-xs">Not compared</span>
|
509 |
</div>
|
510 |
</div>
|
511 |
</div>
|
|
|
515 |
)}
|
516 |
|
517 |
{/* Similarity Results */}
|
518 |
+
<div className="flex-1 overflow-y-auto border border-gray-300 rounded-lg bg-white min-h-0 max-h-[35vh] sm:max-h-[40vh] lg:max-h-none">
|
519 |
+
<div className="p-4 h-full">
|
520 |
+
<h3 className="text-sm font-medium text-gray-700 mb-3 sticky top-0 bg-white z-10">
|
521 |
Cosine Similarities
|
522 |
{selectedExample &&
|
523 |
` (vs "${selectedExample.text.substring(0, 30)}...")`}
|
|
|
531 |
No other examples with embeddings to compare
|
532 |
</div>
|
533 |
) : (
|
534 |
+
<div className="space-y-2 overflow-y-auto max-h-[calc(100%-3rem)]">
|
535 |
{similarities.map((sim) => {
|
536 |
const example = examples.find(
|
537 |
(ex) => ex.id === sim.exampleId
|
|
|
549 |
return (
|
550 |
<div
|
551 |
key={sim.exampleId}
|
552 |
+
className="p-2 sm:p-3 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors"
|
553 |
>
|
554 |
<div className="flex justify-between items-start">
|
555 |
<div className="flex-1 min-w-0">
|