import { useState, useRef, useEffect, useCallback } from 'react' import { Plus, Trash2, 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(true) const [progress, setProgress] = useState<{ completed: number total: number } | null>(null) const { activeWorker, status, modelInfo, hasBeenLoaded, selectedQuantization } = useModel() 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 addExample(newExampleText) setNewExampleText('') }, [newExampleText, addExample]) 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(() => { SAMPLE_TEXTS.forEach((text) => addExample(text)) }, [addExample]) 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 }) } }) console.log({ examples }) 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 */}