RAG Documentation MCP Server

document.addEventListener('DOMContentLoaded', () => { const API_BASE_URL = 'http://localhost:3030'; let toastTimeout; // Pagination state const ITEMS_PER_PAGE = 10; let currentPage = 1; let allDocuments = []; // DOM Elements const docUrlInput = document.getElementById('docUrl'); const extractUrlsBtn = document.getElementById('extractUrlsBtn'); const extractedUrlsSection = document.getElementById('extractedUrls'); const urlList = extractedUrlsSection.querySelector('.url-list'); const addAllUrlsBtn = document.getElementById('addAllUrlsBtn'); const clearUrlsBtn = document.getElementById('clearUrlsBtn'); const clearQueueBtn = document.getElementById('clearQueueBtn'); const queueList = document.getElementById('queueList'); const queueCount = document.getElementById('queueCount'); const documentsList = document.getElementById('documentsList'); const searchQuery = document.getElementById('searchQuery'); const searchBtn = document.getElementById('searchBtn'); const searchResults = document.getElementById('searchResults'); const toast = document.getElementById('toast'); const prevPageBtn = document.getElementById('prevPageBtn'); const nextPageBtn = document.getElementById('nextPageBtn'); const currentPageSpan = document.getElementById('currentPage'); const totalPagesSpan = document.getElementById('totalPages'); // Track queue state for document updates let previousQueueState = []; let previousQueueHtml = ''; let queueUpdateTimer = null; // Track selected documents let selectedDocuments = new Set(); // Check if documents need updating based on queue changes function shouldUpdateDocuments(newQueue) { // If no previous state, just store and return if (previousQueueState.length === 0) { previousQueueState = [...newQueue]; return false; } // If lengths are different, queue items were added/removed if (previousQueueState.length !== newQueue.length) { previousQueueState = [...newQueue]; return false; // No need to update docs when items are just added } // Check if any items changed from PROCESSING to COMPLETED const hasNewCompletions = newQueue.some((item, index) => { const prevItem = previousQueueState[index]; return prevItem && prevItem.status === 'PROCESSING' && item.status === 'COMPLETED'; }); // Only update previous state if we found completions if (hasNewCompletions) { previousQueueState = [...newQueue]; } return hasNewCompletions; } // Helper function to set button loading state with text function setButtonLoading(button, isLoading, loadingText = 'Loading...') { if (isLoading) { button.dataset.originalHtml = button.innerHTML; button.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${loadingText}`; button.classList.add('loading'); button.disabled = true; } else { button.innerHTML = button.dataset.originalHtml; button.classList.remove('loading'); button.disabled = false; delete button.dataset.originalHtml; } } // Toast functionality function showToast(message, type = 'success') { if (toastTimeout) { clearTimeout(toastTimeout); toast.classList.add('hidden'); // Wait for animation to complete setTimeout(() => { displayToast(message, type); }, 300); } else { displayToast(message, type); } } function displayToast(message, type) { const toastMessage = toast.querySelector('.message'); toastMessage.textContent = message; toast.className = `toast ${type}`; toast.classList.remove('hidden'); toastTimeout = setTimeout(() => { toast.classList.add('hidden'); toastTimeout = null; }, 3000); } toast.querySelector('.close').addEventListener('click', () => { toast.classList.add('hidden'); if (toastTimeout) { clearTimeout(toastTimeout); toastTimeout = null; } }); // Extract URLs extractUrlsBtn.addEventListener('click', async () => { const url = docUrlInput.value.trim(); if (!url) { showToast('Please enter a URL', 'error'); return; } setButtonLoading(extractUrlsBtn, true, 'Extracting URLs...'); try { const response = await fetch(`${API_BASE_URL}/extract-urls`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url }) }); if (!response.ok) throw new Error('Failed to extract URLs'); const { urls } = await response.json(); if (urls.length === 0) { showToast('No URLs found', 'error'); return; } urlList.innerHTML = urls.map(url => ` <div class="flex items-center gap-2 p-2 hover:bg-slate-50"> <input type="checkbox" checked class="rounded border-slate-300 text-primary focus:ring-primary"> <span class="text-sm text-slate-700">${url}</span> </div> `).join(''); extractedUrlsSection.classList.remove('hidden'); showToast(`Found ${urls.length} URLs`); } catch (error) { console.error('Error extracting URLs:', error); showToast('Failed to extract URLs', 'error'); } finally { setButtonLoading(extractUrlsBtn, false); } }); // Add all extracted URLs addAllUrlsBtn.addEventListener('click', async () => { const selectedUrls = Array.from(urlList.querySelectorAll('input[type="checkbox"]:checked')) .map(checkbox => checkbox.nextElementSibling.textContent.trim()); if (selectedUrls.length === 0) { showToast('No URLs selected', 'error'); return; } setButtonLoading(addAllUrlsBtn, true, 'Adding URLs...'); try { const response = await fetch(`${API_BASE_URL}/add-doc`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ urls: selectedUrls }) }); if (!response.ok) throw new Error('Failed to add URLs'); showToast(`Added ${selectedUrls.length} URLs to queue`); extractedUrlsSection.classList.add('hidden'); docUrlInput.value = ''; updateQueue(); } catch (error) { console.error('Error adding URLs:', error); showToast('Failed to add URLs to queue', 'error'); } finally { setButtonLoading(addAllUrlsBtn, false); } }); // Clear extracted URLs clearUrlsBtn.addEventListener('click', () => { urlList.innerHTML = ''; extractedUrlsSection.classList.add('hidden'); docUrlInput.value = ''; }); // Update queue list with debounce function startQueuePolling() { if (queueUpdateTimer) { clearInterval(queueUpdateTimer); } queueUpdateTimer = setInterval(updateQueue, 5000); } // Clear queue clearQueueBtn.addEventListener('click', async () => { if (!confirm('Are you sure you want to clear the queue?')) return; setButtonLoading(clearQueueBtn, true, 'Clearing queue...'); try { const response = await fetch(`${API_BASE_URL}/clear-queue`, { method: 'POST' }); if (!response.ok) throw new Error('Failed to clear queue'); // Stop current polling if (queueUpdateTimer) { clearInterval(queueUpdateTimer); } // Reset queue state previousQueueState = []; previousQueueHtml = ''; queueCount.textContent = '(0 items)'; queueList.innerHTML = '<div class="p-8 text-center text-slate-500">No documents in queue</div>'; showToast('Queue cleared successfully'); // Wait for 2 seconds before resuming polling to ensure server has processed the clear setTimeout(() => { updateQueue(); // Do one immediate update startQueuePolling(); // Resume polling }, 2000); } catch (error) { console.error('Error clearing queue:', error); showToast('Failed to clear queue', 'error'); } finally { setButtonLoading(clearQueueBtn, false); } }); // Process queue const processQueueBtn = document.getElementById('processQueueBtn'); processQueueBtn.addEventListener('click', async () => { setButtonLoading(processQueueBtn, true, 'Starting queue...'); try { const response = await fetch(`${API_BASE_URL}/process-queue`, { method: 'POST' }); if (!response.ok) throw new Error('Failed to start queue processing'); showToast('Queue processing started'); // Do an immediate queue update to show status changes updateQueue(); } catch (error) { console.error('Error starting queue:', error); showToast('Failed to start queue processing', 'error'); } finally { setButtonLoading(processQueueBtn, false); } }); // Update queue list async function updateQueue() { try { const response = await fetch(`${API_BASE_URL}/queue`); if (!response.ok) throw new Error('Failed to fetch queue'); const queue = await response.json(); // Generate new HTML const newHtml = generateQueueHtml(queue); // Only update DOM if content has changed if (newHtml !== previousQueueHtml) { queueList.innerHTML = newHtml; previousQueueHtml = newHtml; } // Only update documents if processing completed if (shouldUpdateDocuments(queue)) { updateDocuments(); } } catch (error) { console.error('Error fetching queue:', error); if (!queueList.querySelector('.error-message')) { queueList.innerHTML = '<div class="p-8 text-center text-red-500 error-message"><i class="fas fa-exclamation-circle mr-2"></i>Failed to load queue</div>'; } } } // Generate queue HTML without directly manipulating DOM function generateQueueHtml(queue) { if (!Array.isArray(queue) || queue.length === 0) { queueCount.textContent = '(0 items)'; return '<div class="p-8 text-center text-slate-500">No documents in queue</div>'; } // Update queue count queueCount.textContent = `(${queue.length} item${queue.length === 1 ? '' : 's'})`; // Group items by status const groupedByStatus = queue.reduce((acc, item) => { const status = item.status.toUpperCase(); if (!acc[status]) acc[status] = []; acc[status].push(item); return acc; }, {}); // Generate HTML for grouped items return Object.entries(groupedByStatus).map(([status, items]) => ` <div class="status-group"> <div class="bg-slate-50 px-4 py-2 text-sm font-medium text-slate-700"> ${status} (${items.length}) </div> ${items.map(item => ` <div class="p-4 flex justify-between items-center hover:bg-slate-50 group"> <div class="flex-1"> <div class="text-sm font-medium text-slate-700 truncate">${item.url}</div> <div class="text-xs text-slate-500">Added: ${new Date(item.timestamp).toLocaleString()}</div> </div> <div class="flex items-center gap-2"> <span class="px-2 py-1 text-xs rounded-full ${getStatusClass(item.status)}">${item.status}</span> </div> </div> `).join('')} </div> `).join(''); } // Pagination functions function updatePaginationControls(totalItems) { const totalPages = Math.max(1, Math.ceil(totalItems / ITEMS_PER_PAGE)); currentPageSpan.textContent = currentPage; totalPagesSpan.textContent = totalPages; prevPageBtn.disabled = currentPage === 1; nextPageBtn.disabled = currentPage === totalPages || totalItems === 0; // If current page is greater than total pages, reset to last page if (currentPage > totalPages) { currentPage = totalPages; displayDocuments(allDocuments); } } function getPageItems(items, page) { const start = (page - 1) * ITEMS_PER_PAGE; return items.slice(start, Math.min(start + ITEMS_PER_PAGE, items.length)); } prevPageBtn.addEventListener('click', () => { if (currentPage > 1) { currentPage--; displayDocuments(allDocuments); } }); nextPageBtn.addEventListener('click', () => { const totalPages = Math.ceil(allDocuments.length / ITEMS_PER_PAGE); if (currentPage < totalPages) { currentPage++; displayDocuments(allDocuments); } }); // Get status class for queue items function getStatusClass(status) { switch (status.toUpperCase()) { case 'PENDING': return 'bg-yellow-100 text-yellow-800'; case 'PROCESSING': return 'bg-blue-100 text-blue-800'; case 'COMPLETED': return 'bg-green-100 text-green-800'; case 'FAILED': return 'bg-red-100 text-red-800'; default: return 'bg-slate-100 text-slate-800'; } } // Update documents list async function updateDocuments() { documentsList.innerHTML = '<div class="p-8 text-center text-slate-500"><i class="fas fa-spinner fa-spin mr-2"></i>Loading documents...</div>'; try { const response = await fetch(`${API_BASE_URL}/documents`); if (!response.ok) throw new Error('Failed to fetch documents'); const documents = await response.json(); allDocuments = documents; displayDocuments(documents); } catch (error) { console.error('Error fetching documents:', error); documentsList.innerHTML = '<div class="p-8 text-center text-red-500"><i class="fas fa-exclamation-circle mr-2"></i>Failed to load documents</div>'; } } // Display documents function displayDocuments(documents) { if (documents.length === 0) { documentsList.innerHTML = '<div class="p-8 text-center text-slate-500">No documents available</div>'; updatePaginationControls(0); return; } const pageItems = getPageItems(documents, currentPage); // Add header with select all and bulk actions documentsList.innerHTML = ` <div class="p-4 bg-slate-50 border-b border-slate-200 flex items-center justify-between"> <div class="flex items-center gap-4"> <div class="flex items-center gap-2"> <input type="checkbox" id="selectAll" class="rounded border-slate-300 text-primary focus:ring-primary" ${pageItems.length > 0 && selectedDocuments.size === pageItems.length ? 'checked' : ''}> <label for="selectAll" class="text-sm text-slate-600">Select All</label> </div> <span class="text-sm text-slate-500 border-l border-slate-300 pl-4"> Total: ${documents.length} document${documents.length === 1 ? '' : 's'} </span> </div> <div class="flex items-center gap-2"> <div class="bulk-actions ${selectedDocuments.size > 0 ? '' : 'hidden'}"> <span class="text-sm text-slate-600 mr-2">${selectedDocuments.size} selected</span> <button id="removeSelected" class="btn-danger"> <i class="fas fa-trash"></i> Remove Selected </button> </div> <button id="removeAll" class="btn-danger ${documents.length > 0 ? '' : 'hidden'}"> <i class="fas fa-trash"></i> Remove All </button> </div> </div> ${pageItems.map(doc => ` <div class="p-4 flex justify-between items-center hover:bg-slate-50 group"> <div class="flex items-center gap-4 flex-1"> <input type="checkbox" class="doc-checkbox rounded border-slate-300 text-primary focus:ring-primary" data-url="${doc.url}" ${selectedDocuments.has(doc.url) ? 'checked' : ''}> <div> <div class="font-medium text-slate-700">${doc.title}</div> <div class="text-sm text-slate-500 truncate">${doc.url}</div> <div class="text-xs text-slate-400">Added: ${new Date(doc.timestamp).toLocaleString()}</div> </div> </div> <button class="btn-danger remove-doc opacity-0 group-hover:opacity-100 transition-opacity" data-url="${doc.url}"> <i class="fas fa-trash"></i> <span class="tooltip">Click to remove</span> </button> </div> `).join('')} `; updatePaginationControls(documents.length); // Add event listeners const selectAllCheckbox = documentsList.querySelector('#selectAll'); const removeSelectedBtn = documentsList.querySelector('#removeSelected'); const removeAllBtn = documentsList.querySelector('#removeAll'); const checkboxes = documentsList.querySelectorAll('.doc-checkbox'); const bulkActions = documentsList.querySelector('.bulk-actions'); // Select All functionality selectAllCheckbox?.addEventListener('change', () => { checkboxes.forEach(checkbox => { checkbox.checked = selectAllCheckbox.checked; const url = checkbox.dataset.url; if (selectAllCheckbox.checked) { selectedDocuments.add(url); } else { selectedDocuments.delete(url); } }); bulkActions?.classList.toggle('hidden', selectedDocuments.size === 0); }); // Individual checkbox functionality checkboxes.forEach(checkbox => { checkbox.addEventListener('change', () => { const url = checkbox.dataset.url; if (checkbox.checked) { selectedDocuments.add(url); } else { selectedDocuments.delete(url); } selectAllCheckbox.checked = checkboxes.length === selectedDocuments.size; bulkActions?.classList.toggle('hidden', selectedDocuments.size === 0); }); }); // Remove Selected functionality removeSelectedBtn?.addEventListener('click', async () => { if (selectedDocuments.size === 0) return; if (!confirm(`Are you sure you want to remove ${selectedDocuments.size} selected document${selectedDocuments.size === 1 ? '' : 's'}?`)) return; setButtonLoading(removeSelectedBtn, true, 'Removing...'); try { const response = await fetch(`${API_BASE_URL}/documents`, { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ urls: Array.from(selectedDocuments) }) }); if (!response.ok) throw new Error('Failed to remove documents'); const result = await response.json(); showToast(result.message); selectedDocuments.clear(); updateDocuments(); } catch (error) { console.error('Error removing documents:', error); showToast('Failed to remove documents', 'error'); } finally { setButtonLoading(removeSelectedBtn, false); } }); // Remove All functionality removeAllBtn?.addEventListener('click', async () => { if (!confirm('Are you sure you want to remove ALL documents? This action cannot be undone.')) return; setButtonLoading(removeAllBtn, true, 'Removing all...'); try { const response = await fetch(`${API_BASE_URL}/documents/all`, { method: 'DELETE' }); if (!response.ok) throw new Error('Failed to remove all documents'); const result = await response.json(); showToast(result.message); selectedDocuments.clear(); updateDocuments(); } catch (error) { console.error('Error removing all documents:', error); showToast('Failed to remove all documents', 'error'); } finally { setButtonLoading(removeAllBtn, false); } }); // Individual remove buttons documentsList.querySelectorAll('.remove-doc').forEach(button => { button.addEventListener('click', async () => { const url = button.dataset.url; if (!confirm('Are you sure you want to remove this document?')) return; button.classList.add('deleting'); button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Removing...'; try { const response = await fetch(`${API_BASE_URL}/documents`, { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url }) }); if (!response.ok) throw new Error('Failed to remove document'); const result = await response.json(); showToast(result.message); selectedDocuments.delete(url); updateDocuments(); } catch (error) { console.error('Error removing document:', error); showToast('Failed to remove document', 'error'); button.classList.remove('deleting'); button.innerHTML = '<i class="fas fa-trash"></i><span class="tooltip">Click to remove</span>'; } }); }); } // Display search results function displaySearchResults(results) { if (!Array.isArray(results) || results.length === 0) { searchResults.innerHTML = '<div class="p-8 text-center text-slate-500">No results found</div>'; return; } searchResults.innerHTML = results.map(result => ` <div class="p-4 border border-slate-200 rounded-md hover:bg-slate-50"> <a href="${result.url}" target="_blank" class="font-medium text-primary hover:underline">${result.title}</a> <p class="text-sm text-slate-600 mt-2">${result.snippet || result.content}</p> <div class="text-xs text-slate-400 mt-1 truncate">${result.url}</div> </div> `).join(''); } // Search documentation searchBtn.addEventListener('click', async () => { const query = searchQuery.value.trim(); if (!query) { showToast('Please enter a search query', 'error'); return; } setButtonLoading(searchBtn, true, 'Searching...'); try { const response = await fetch(`${API_BASE_URL}/search`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query }) }); if (!response.ok) throw new Error('Failed to search'); const data = await response.json(); displaySearchResults(data.results); } catch (error) { console.error('Error searching:', error); showToast('Failed to search documentation', 'error'); searchResults.innerHTML = '<div class="text-center text-slate-500 p-4">Search failed. Please try again.</div>'; } finally { setButtonLoading(searchBtn, false); } }); // Handle Enter key in search input searchQuery.addEventListener('keypress', (e) => { if (e.key === 'Enter') { searchBtn.click(); } }); // Add URL on Enter key docUrlInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { extractUrlsBtn.click(); } }); // Initial updates updateQueue(); updateDocuments(); // Start polling startQueuePolling(); });