/** * RAG Axiom Extractor - Main Application Logic * Handles UI, document processing, and worker communication */ class RAGApplication { constructor() { this.worker = null; this.documents = new Map(); this.axioms = []; this.embeddings = new Map(); this.isInitialized = false; this.init(); } init() { // Initialize web worker this.worker = new Worker('worker.js'); // Set up worker message handlers this.worker.onmessage = (e) => this.handleWorkerMessage(e); this.worker.onerror = (e) => this.handleWorkerError(e); // Set up UI event listeners this.setupEventListeners(); // Load saved data from localStorage this.loadSavedData(); } setupEventListeners() { // File upload const fileInput = document.getElementById('fileInput'); const uploadArea = document.getElementById('uploadArea'); fileInput.addEventListener('change', (e) => this.handleFileUpload(e.target.files)); // Drag and drop uploadArea.addEventListener('dragover', (e) => { e.preventDefault(); uploadArea.classList.add('dragover'); }); uploadArea.addEventListener('dragleave', () => { uploadArea.classList.remove('dragover'); }); uploadArea.addEventListener('drop', (e) => { e.preventDefault(); uploadArea.classList.remove('dragover'); this.handleFileUpload(e.dataTransfer.files); }); // Generation document.getElementById('generateBtn').addEventListener('click', () => this.generateResponse()); document.getElementById('queryInput').addEventListener('keypress', (e) => { if (e.key === 'Enter' && e.ctrlKey) { this.generateResponse(); } }); // Actions document.getElementById('clearAllBtn').addEventListener('click', () => this.clearAllData()); document.getElementById('exportBtn').addEventListener('click', () => this.exportAxioms()); } handleWorkerMessage(e) { const { type, payload } = e.data; switch (type) { case 'progress': this.updateProgress(payload); break; case 'status': this.updateStatus(payload.model, payload.status); break; case 'ready': this.isInitialized = true; this.updateUI(); break; case 'embeddingComplete': this.handleEmbeddingComplete(payload); break; case 'axiomExtractionComplete': this.handleAxiomExtractionComplete(payload); break; case 'generationComplete': this.handleGenerationComplete(payload); break; case 'error': this.handleError(payload.error); break; } } handleWorkerError(error) { console.error('Worker error:', error); this.showNotification('Worker error occurred. Check console for details.', 'error'); } updateProgress({ progress, model, loaded, total }) { const progressContainer = document.getElementById('progressContainer'); const progressBar = document.getElementById('progressBar'); const progressText = document.getElementById('progressText'); progressContainer.classList.remove('hidden'); progressBar.style.width = `${progress * 100}%`; progressText.textContent = `${Math.round(progress * 100)}% (${(loaded / 1024 / 1024).toFixed(1)}MB / ${(total / 1024 / 1024).toFixed(1)}MB)`; } updateStatus(model, status) { const statusElement = model === 'embedding' ? document.getElementById('embeddingStatus') : document.getElementById('generationStatus'); statusElement.textContent = status; statusElement.className = `status ${status.includes('✅') ? 'ready' : 'pending'}`; } async handleFileUpload(files) { if (!this.isInitialized) { this.showNotification('Please wait for models to finish loading.', 'warning'); return; } const uploadStatus = document.getElementById('uploadStatus'); uploadStatus.innerHTML = 'Processing files...'; for (const file of files) { try { const text = await this.readFile(file); const docId = `${file.name}-${Date.now()}`; // Store document this.documents.set(docId, { id: docId, name: file.name, size: file.size, content: text, chunks: this.chunkText(text), uploadedAt: new Date().toISOString() }); // Generate embeddings for chunks this.worker.postMessage({ type: 'generateEmbeddings', payload: { docId, chunks: this.documents.get(docId).chunks } }); } catch (error) { console.error(`Error processing ${file.name}:`, error); this.showNotification(`Error processing ${file.name}`, 'error'); } } uploadStatus.innerHTML = 'Files processed! Generating embeddings...'; setTimeout(() => uploadStatus.innerHTML = '', 3000); this.saveData(); } readFile(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = (e) => resolve(e.target.result); reader.onerror = (e) => reject(e); reader.readAsText(file); }); } chunkText(text, chunkSize = 200, overlap = 50) { const words = text.split(/\s+/); const chunks = []; for (let i = 0; i < words.length; i += chunkSize - overlap) { const chunk = words.slice(i, i + chunkSize).join(' '); if (chunk.length > 50) { chunks.push(chunk); } } return chunks; } handleEmbeddingComplete({ docId, embeddings }) { this.embeddings.set(docId, embeddings); this.extractAxioms(docId); this.updateDocumentList(); } extractAxioms(docId) { const document = this.documents.get(docId); if (!document) return; this.worker.postMessage({ type: 'extractAxioms', payload: { docId, text: document.content } }); } handleAxiomExtractionComplete({ axioms }) { this.axioms.push(...axioms); this.updateAxiomList(); this.saveData(); } async generateResponse() { const query = document.getElementById('queryInput').value.trim(); if (!query) { this.showNotification('Please enter a query.', 'warning'); return; } if (!this.isInitialized) { this.showNotification('Models are still loading.', 'warning'); return; } const useAxioms = document.getElementById('useAxioms').checked; const useContext = document.getElementById('useContext').checked; // Show loading state const generateBtn = document.getElementById('generateBtn'); const statusIndicator = document.getElementById('generationStatusIndicator'); generateBtn.disabled = true; generateBtn.textContent = 'Generating...'; statusIndicator.innerHTML = 'Retrieving relevant context...'; // Prepare context let context = ''; let retrievedChunks = []; if (useContext && this.embeddings.size > 0) { // Retrieve relevant chunks retrievedChunks = await this.retrieveRelevantChunks(query, 5); context = retrievedChunks.map(c => c.text).join('\n\n'); } if (useAxioms && this.axioms.length > 0) { // Add axioms to context const relevantAxioms = this.getRelevantAxioms(query); if (relevantAxioms.length > 0) { context += '\n\n=== EXTRACTED AXIOMS ===\n'; context += relevantAxioms.map(a => `• ${a.text}`).join('\n'); } } // Show retrieved context this.updateRetrievedContext(retrievedChunks); statusIndicator.innerHTML = 'Generating response...'; // Send generation request to worker this.worker.postMessage({ type: 'generate', payload: { query, context, maxLength: 512 } }); } async retrieveRelevantChunks(query, topK = 5) { if (this.embeddings.size === 0) return []; // Get query embedding const queryEmbedding = await this.getQueryEmbedding(query); // Calculate similarities const scores = []; for (const [docId, docEmbeddings] of this.embeddings) { for (let i = 0; i < docEmbeddings.length; i++) { const similarity = this.cosineSimilarity(queryEmbedding, docEmbeddings[i]); scores.push({ docId, chunkIndex: i, similarity, text: this.documents.get(docId).chunks[i] }); } } // Sort by similarity and return top K return scores .sort((a, b) => b.similarity - a.similarity) .slice(0, topK); } async getQueryEmbedding(query) { return new Promise((resolve) => { const messageId = `embedding-${Date.now()}`; const handleResponse = (e) => { if (e.data.type === 'queryEmbeddingComplete' && e.data.payload.messageId === messageId) { this.worker.removeEventListener('message', handleResponse); resolve(e.data.payload.embedding); } }; this.worker.addEventListener('message', handleResponse); this.worker.postMessage({ type: 'generateQueryEmbedding', payload: { query, messageId } }); }); } cosineSimilarity(a, b) { if (!a || !b) return 0; const dotProduct = a.reduce((sum, val, i) => sum + val * b[i], 0); const magnitudeA = Math.sqrt(a.reduce((sum, val) => sum + val * val, 0)); const magnitudeB = Math.sqrt(b.reduce((sum, val) => sum + val * val, 0)); return magnitudeA && magnitudeB ? dotProduct / (magnitudeA * magnitudeB) : 0; } getRelevantAxioms(query, topK = 10) { // Simple keyword matching for relevant axioms const queryWords = query.toLowerCase().split(/\s+/).filter(w => w.length > 3); return this.axioms .map(axiom => { const axiomWords = axiom.text.toLowerCase().split(/\s+/); const matches = queryWords.filter(w => axiomWords.includes(w)).length; return { ...axiom, relevance: matches }; }) .filter(a => a.relevance > 0) .sort((a, b) => b.relevance - a.relevance) .slice(0, topK); } updateRetrievedContext(chunks) { const contextDiv = document.getElementById('retrievedContext'); if (chunks.length === 0) { contextDiv.innerHTML = '
No relevant context found.
'; return; } contextDiv.innerHTML = chunks.map((chunk, i) => `${chunk.text}
No documents indexed yet. Upload files to begin.
'; return; } listContainer.innerHTML = Array.from(this.documents.values()).map(doc => `No axioms extracted yet. Upload and index documents to extract axioms.
'; return; } listContainer.innerHTML = this.axioms.map((axiom, i) => `${axiom.text}