import { useState, useRef, useEffect, useCallback } from 'react' import { Plus, Eraser, Loader2, X, Eye, EyeOff } from 'lucide-react' import { EmbeddingExample, FeatureExtractionWorkerInput, WorkerMessage } from '../../types' import { useModel } from '../../contexts/ModelContext' import { useFeatureExtraction } from '../../contexts/FeatureExtractionContext' interface Point2D { x: number y: number id: string text: string similarity?: number } // Sample data for quick testing const SAMPLE_TEXTS = [ 'The cat sat on the mat', 'A feline rested on the carpet', 'I love programming in JavaScript', 'JavaScript development is my passion', 'The weather is beautiful today', "It's a sunny and warm day outside", 'Machine learning is transforming technology', 'AI and deep learning are revolutionizing computing', 'I enjoy reading books in the evening', 'Pizza is one of my favorite foods' ] function FeatureExtraction() { const { examples, selectedExample, setSelectedExample, similarities, addExample, removeExample, updateExample, calculateSimilarities, clearExamples, config } = useFeatureExtraction() const [newExampleText, setNewExampleText] = useState('') const [isExtracting, setIsExtracting] = useState(false) const [showVisualization, setShowVisualization] = useState(() => { if (typeof window !== 'undefined') { return window.innerWidth >= 768 } return true }) const [progress, setProgress] = useState<{ completed: number total: number } | null>(null) const { activeWorker, status, modelInfo, hasBeenLoaded, selectedQuantization } = useModel() useEffect(() => { const handleResize = () => { setShowVisualization(window.innerWidth >= 768) } window.addEventListener('resize', handleResize) return () => { window.removeEventListener('resize', handleResize) } }, []) const chartRef = useRef(null) // PCA reduction to 2D for visualization const reduceTo2D = useCallback( (embeddings: number[][]): Point2D[] => { if (embeddings.length === 0) return [] // For simplicity, just use first 2 dimensions if available, or random projection const points: Point2D[] = examples .filter((ex) => ex.embedding) .map((example, i) => { const emb = example.embedding! let x, y if (emb.length >= 2) { x = emb[0] y = emb[1] } else { // Simple hash-based positioning for 1D embeddings const hash = example.text.split('').reduce((a, b) => { a = (a << 5) - a + b.charCodeAt(0) return a & a }, 0) x = Math.sin(hash) * 100 y = Math.cos(hash) * 100 } return { x, y, id: example.id, text: example.text, similarity: similarities.find((s) => s.exampleId === example.id) ?.similarity } }) // Normalize points to fit in chart if (points.length > 0) { const minX = Math.min(...points.map((p) => p.x)) const maxX = Math.max(...points.map((p) => p.x)) const minY = Math.min(...points.map((p) => p.y)) const maxY = Math.max(...points.map((p) => p.y)) const rangeX = maxX - minX || 1 const rangeY = maxY - minY || 1 return points.map((p) => ({ ...p, x: ((p.x - minX) / rangeX) * 300 + 50, y: ((p.y - minY) / rangeY) * 200 + 50 })) } return points }, [examples, similarities] ) const extractEmbeddings = useCallback( async (textsToExtract: string[]) => { if (!modelInfo || !activeWorker || textsToExtract.length === 0) return setIsExtracting(true) setProgress({ completed: 0, total: textsToExtract.length }) const message: FeatureExtractionWorkerInput = { type: 'extract', texts: textsToExtract, model: modelInfo.id, dtype: selectedQuantization ?? 'fp32', config } activeWorker.postMessage(message) }, [modelInfo, activeWorker, selectedQuantization, config] ) const handleAddExample = useCallback(() => { if (!newExampleText.trim()) return // Check for duplicates const trimmedText = newExampleText.trim() const isDuplicate = examples.some( (example) => example.text.toLowerCase() === trimmedText.toLowerCase() ) if (isDuplicate) { // Optionally show a toast or alert here setNewExampleText('') return } addExample(trimmedText) setNewExampleText('') }, [newExampleText, addExample, examples]) const handleExtractAll = useCallback(() => { const textsToExtract = examples .filter((ex) => !ex.embedding && !ex.isLoading) .map((ex) => ex.text) if (textsToExtract.length > 0) { extractEmbeddings(textsToExtract) } }, [examples, extractEmbeddings]) const handleSelectExample = useCallback( (example: EmbeddingExample) => { setSelectedExample(example) if (example.embedding) { calculateSimilarities(example) } }, [setSelectedExample, calculateSimilarities] ) const handleLoadSampleData = useCallback(() => { const existingTexts = new Set(examples.map((ex) => ex.text.toLowerCase())) SAMPLE_TEXTS.forEach((text) => { if (!existingTexts.has(text.toLowerCase())) { addExample(text) } }) }, [addExample, examples]) useEffect(() => { if (!activeWorker) return const onMessageReceived = (e: MessageEvent) => { const { status, output } = e.data if (status === 'progress' && output) { setProgress({ completed: output.completed, total: output.total }) if (output.embedding && output.currentText) { const example = examples.find((ex) => ex.text === output.currentText) if (example) { updateExample(example.id, { embedding: output.embedding, isLoading: false }) } } } else if (status === 'output' && output?.embeddings) { output.embeddings.forEach((result: any) => { const example = examples.find((ex) => ex.text === result.text) if (example) { updateExample(example.id, { embedding: result.embedding, isLoading: false }) } }) setIsExtracting(false) setProgress(null) } else if (status === 'error') { setIsExtracting(false) setProgress(null) } } activeWorker.addEventListener('message', onMessageReceived) return () => activeWorker.removeEventListener('message', onMessageReceived) }, [activeWorker, examples, updateExample]) const handleKeyPress = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault() handleAddExample() } } const points2D = reduceTo2D( examples.filter((ex) => ex.embedding).map((ex) => ex.embedding!) ) const busy = status !== 'ready' || isExtracting return (

Feature Extraction (Embeddings)

{/* Left Panel - Examples */}
{/* Add Example */}