CoolFace
Apppublic

agentpixel/pinecone-knowledge-indexer

sourceHugging Faceupdated 3mo agoView on Hugging Face
0likes
script.js583 linesDownload Raw Back to static
1document.addEventListener('DOMContentLoaded', () => {2    // State Variables3    let selectedFile = null;4 5    // DOM Elements6    const dropzone = document.getElementById('dropzone');7    const fileInput = document.getElementById('file-input');8    const fileDetails = document.getElementById('file-details');9    const fileNameEl = document.getElementById('file-name');10    const fileSizeEl = document.getElementById('file-size');11    const btnRemove = document.getElementById('btn-remove');12    const btnIngest = document.getElementById('btn-ingest');13    14    const loadingStepper = document.getElementById('loading-stepper');15    16    // Steps17    const stepParsing = document.getElementById('step-parsing');18    const stepEmbedding = document.getElementById('step-embedding');19    const stepIngestion = document.getElementById('step-ingestion');20    21    // Indicators22    const indParsing = document.getElementById('ind-parsing');23    const indEmbedding = document.getElementById('ind-embedding');24    const indIngestion = document.getElementById('ind-ingestion');25 26    // Progress Bars27    const pbParsing = document.getElementById('pb-parsing');28    const pbEmbedding = document.getElementById('pb-embedding');29    const pbIngestion = document.getElementById('pb-ingestion');30 31    // Meta Texts32    const metaParsing = document.getElementById('meta-parsing');33    const metaEmbedding = document.getElementById('meta-embedding');34    const metaIngestion = document.getElementById('meta-ingestion');35    36    // Ingestion Results37    const ingestResult = document.getElementById('ingest-result');38    const resDocId = document.getElementById('res-doc-id');39    const resChunks = document.getElementById('res-chunks');40    const resVectors = document.getElementById('res-vectors');41    const resTime = document.getElementById('res-time');42    43    // Search44    const searchForm = document.getElementById('search-form');45    const searchQuery = document.getElementById('search-query');46    const btnSearch = document.getElementById('btn-search');47    const resultsContainer = document.getElementById('results-container');48    const searchMeta = document.getElementById('search-meta');49    const resultsCount = document.getElementById('results-count');50    51    // Health / Stats52    const statusIndicator = document.getElementById('status-indicator');53    const statusText = document.getElementById('status-text');54    const statConnection = document.getElementById('stat-connection');55    const statVectors = document.getElementById('stat-vectors');56    const statDimension = document.getElementById('stat-dimension');57    const radarPing = document.querySelector('.radar-ping');58    const radarGlow = document.querySelector('.radar-glow');59 60    // Toast Container61    const toastContainer = document.getElementById('toast-container');62 63    // Toast Notification System64    function showToast(title, message, type = 'info') {65        const toast = document.createElement('div');66        toast.className = `toast toast-${type}`;67        68        let iconHtml = '';69        if (type === 'success') {70            iconHtml = '<i class="ph-fill ph-check-circle toast-icon"></i>';71        } else if (type === 'error') {72            iconHtml = '<i class="ph-fill ph-x-circle toast-icon"></i>';73        } else {74            iconHtml = '<i class="ph-fill ph-info toast-icon"></i>';75        }76 77        toast.innerHTML = `78            <div class="toast-icon">${iconHtml}</div>79            <div class="toast-content">80                <div class="toast-title">${escapeHtml(title)}</div>81                <div class="toast-msg">${escapeHtml(message)}</div>82            </div>83            <button class="toast-close" title="Close"><i class="ph ph-x"></i></button>84        `;85 86        toastContainer.appendChild(toast);87        88        // Trigger transition89        setTimeout(() => toast.classList.add('show'), 10);90 91        const closeToast = () => {92            toast.classList.remove('show');93            setTimeout(() => toast.remove(), 400);94        };95 96        toast.querySelector('.toast-close').addEventListener('click', closeToast);97        98        // Auto-close after 5 seconds99        setTimeout(closeToast, 5000);100    }101 102    // Segmented Tabs Navigation Pill Slider Setup103    const tabsNav = document.querySelector('.tabs-nav');104    const tabButtons = document.querySelectorAll('.tab-btn');105    const tabContents = document.querySelectorAll('.tab-content');106 107    // Inject active slider background pill108    const tabSlider = document.createElement('div');109    tabSlider.className = 'tab-slider';110    tabsNav.appendChild(tabSlider);111 112    function updateSlider(activeBtn) {113        if (!activeBtn) return;114        tabSlider.style.width = `${activeBtn.offsetWidth}px`;115        tabSlider.style.transform = `translateX(${activeBtn.offsetLeft - 4}px)`;116    }117 118    tabButtons.forEach(button => {119        button.addEventListener('click', () => {120            const targetTab = button.getAttribute('data-tab');121            122            // Toggle active classes123            tabButtons.forEach(btn => btn.classList.remove('active'));124            button.classList.add('active');125            updateSlider(button);126            127            // Toggle active contents with fade/slide animations128            tabContents.forEach(content => {129                if (content.id === targetTab) {130                    content.classList.add('active');131                } else {132                    content.classList.remove('active');133                }134            });135 136            // If entering Stats tab, refresh stats137            if (targetTab === 'stats-tab') {138                refreshStats();139            }140        });141    });142 143    // Update slider position on resize144    window.addEventListener('resize', () => {145        const activeTab = document.querySelector('.tab-btn.active');146        if (activeTab) updateSlider(activeTab);147    });148 149    // Initialize Slider Position150    const initialActiveTab = document.querySelector('.tab-btn.active');151    if (initialActiveTab) {152        // Run after DOM settles to measure dimensions accurately153        setTimeout(() => updateSlider(initialActiveTab), 50);154    }155 156    // Connection Health Verification157    async function checkHealth() {158        try {159            const res = await fetch('/api/health');160            const data = await res.json();161            162            if (data.pinecone_configured) {163                statusIndicator.className = 'status-dot connected';164                statusText.textContent = 'Pipeline Connected';165                statConnection.textContent = 'Connected';166                statConnection.className = 'stat-number green';167                168                if (radarPing && radarGlow) {169                    radarPing.style.backgroundColor = 'var(--accent)';170                    radarPing.style.boxShadow = '0 0 10px var(--accent)';171                    radarGlow.style.borderColor = 'var(--accent)';172                    radarGlow.style.animation = 'radar-grow 2.5s cubic-bezier(0.1, 0.8, 0.3, 1) infinite';173                }174            } else {175                statusIndicator.className = 'status-dot disconnected';176                statusText.textContent = 'API Key Missing';177                statConnection.textContent = 'Key Missing';178                statConnection.className = 'stat-number';179                statConnection.style.color = 'var(--error)';180                181                if (radarPing && radarGlow) {182                    radarPing.style.backgroundColor = 'var(--error)';183                    radarPing.style.boxShadow = '0 0 10px var(--error)';184                    radarGlow.style.borderColor = 'var(--error)';185                    radarGlow.style.animation = 'none';186                }187                showToast('Configuration Warning', 'PINECONE_API_KEY is not set in the pipeline backend.', 'info');188            }189        } catch (e) {190            statusIndicator.className = 'status-dot disconnected';191            statusText.textContent = 'Offline';192            statConnection.textContent = 'Offline';193            statConnection.className = 'stat-number';194            statConnection.style.color = 'var(--error)';195            196            if (radarPing && radarGlow) {197                radarPing.style.backgroundColor = 'var(--error)';198                radarPing.style.boxShadow = '0 0 10px var(--error)';199                radarGlow.style.borderColor = 'var(--error)';200                radarGlow.style.animation = 'none';201            }202            showToast('Pipeline Status', 'FastAPI backend connection is offline.', 'error');203        }204    }205 206    // Refresh Statistics207    async function refreshStats() {208        try {209            const res = await fetch('/api/stats');210            const data = await res.json();211            212            if (data.exists) {213                statVectors.textContent = data.total_vector_count.toLocaleString();214                statDimension.textContent = data.dimension;215            } else {216                statVectors.textContent = '0';217            }218        } catch (e) {219            console.error('Failed to load stats:', e);220        }221    }222 223    // Initialize Connection Checks224    checkHealth();225    refreshStats();226 227    // Drag and Drop File Handlers228    dropzone.addEventListener('click', () => fileInput.click());229 230    dropzone.addEventListener('dragover', (e) => {231        e.preventDefault();232        dropzone.classList.add('dragover');233    });234 235    dropzone.addEventListener('dragleave', () => {236        dropzone.classList.remove('dragover');237    });238 239    dropzone.addEventListener('drop', (e) => {240        e.preventDefault();241        dropzone.classList.remove('dragover');242        if (e.dataTransfer.files.length > 0) {243            handleFileSelection(e.dataTransfer.files[0]);244        }245    });246 247    fileInput.addEventListener('change', (e) => {248        if (e.target.files.length > 0) {249            handleFileSelection(e.target.files[0]);250        }251    });252 253    function handleFileSelection(file) {254        selectedFile = file;255        256        // Format size257        let sizeStr = '';258        if (file.size < 1024) sizeStr = file.size + ' B';259        else if (file.size < 1048576) sizeStr = (file.size / 1024).toFixed(1) + ' KB';260        else sizeStr = (file.size / 1048576).toFixed(1) + ' MB';261 262        fileNameEl.textContent = file.name;263        fileSizeEl.textContent = sizeStr;264        265        dropzone.style.display = 'none';266        fileDetails.style.display = 'flex';267        btnIngest.disabled = false;268        269        // Reset old results when selecting a new file270        ingestResult.style.display = 'none';271        loadingStepper.style.display = 'none';272        273        showToast('Document Selected', `Ready to ingest "${file.name}" into Pinecone.`, 'info');274    }275 276    // Remove File Handler277    btnRemove.addEventListener('click', (e) => {278        e.stopPropagation();279        resetUploadUI();280    });281 282    function resetUploadUI() {283        selectedFile = null;284        fileInput.value = '';285        dropzone.style.display = 'flex';286        fileDetails.style.display = 'none';287        btnIngest.disabled = true;288        btnIngest.innerHTML = '<i class="ph ph-lightning"></i> Start Pipeline Ingestion';289        loadingStepper.style.display = 'none';290        ingestResult.style.display = 'none';291    }292 293    // Ingestion Processing Handler with SSE Stream parsing294    btnIngest.addEventListener('click', async () => {295        if (!selectedFile) return;296 297        // UI Updates: Disable buttons & prepare steps298        btnIngest.disabled = true;299        btnRemove.style.display = 'none';300        btnIngest.innerHTML = '<i class="ph ph-spinner step-spinner"></i> Processing Pipeline...';301        302        loadingStepper.style.display = 'flex';303        ingestResult.style.display = 'none';304        305        resetSteps();306        307        const formData = new FormData();308        formData.append('file', selectedFile);309 310        try {311            const response = await fetch('/api/upload', {312                method: 'POST',313                body: formData314            });315 316            if (!response.ok) {317                const errData = await response.json();318                throw new Error(errData.detail || 'Inference pipeline connection failed.');319            }320 321            // Stream Reader Setup322            const reader = response.body.getReader();323            const decoder = new TextDecoder();324            let buffer = '';325 326            while (true) {327                const { done, value } = await reader.read();328                if (done) break;329 330                buffer += decoder.decode(value, { stream: true });331                const lines = buffer.split('\n');332                buffer = lines.pop(); // Hold onto the incomplete trailing line333 334                for (const line of lines) {335                    const trimmed = line.trim();336                    if (!trimmed) continue;337 338                    if (trimmed.startsWith('data:')) {339                        const dataStr = trimmed.substring(5).trim();340                        try {341                            const eventData = JSON.parse(dataStr);342                            handleProgressEvent(eventData);343                        } catch (err) {344                            console.error('JSON parsing error on line:', dataStr, err);345                        }346                    }347                }348            }349 350        } catch (error) {351            showToast('Ingestion Error', error.message, 'error');352            resetUploadUI();353        }354    });355 356    // Handle streamed events from backend357    function handleProgressEvent(data) {358        if (data.status === 'error') {359            throw new Error(data.detail || 'Ingestion failed.');360        }361        362        if (data.status === 'success') {363            // Set all steps complete364            setStepComplete(stepParsing, indParsing, pbParsing, "Completed");365            setStepComplete(stepEmbedding, indEmbedding, pbEmbedding, "Completed");366            setStepComplete(stepIngestion, indIngestion, pbIngestion, "Completed");367            368            // Populate Success Stats369            resDocId.textContent = data.document_id;370            resChunks.textContent = data.chunks_count;371            resVectors.textContent = data.upserted_count;372            resTime.textContent = data.elapsed_seconds + 's';373 374            // Show results375            setTimeout(() => {376                loadingStepper.style.display = 'none';377                ingestResult.style.display = 'block';378                btnIngest.innerHTML = '<i class="ph ph-check"></i> Ingestion Successful';379                btnRemove.style.display = 'flex';380                showToast('Ingestion Success', `Successfully indexed ${data.chunks_count} chunks in ${data.elapsed_seconds}s.`, 'success');381                refreshStats();382            }, 800);383            return;384        }385 386        const stage = data.stage;387        const progress = data.progress || 0;388 389        if (stage === 'parsing') {390            setStepActive(stepParsing, indParsing);391            pbParsing.style.width = progress + '%';392            metaParsing.textContent = `Parsing: ${progress}%`;393            394            if (data.status === 'complete') {395                setStepComplete(stepParsing, indParsing, pbParsing, `Parsed ${data.total_chunks} chunks`);396            }397        } 398        else if (stage === 'embedding') {399            // Guarantee previous step is closed out400            if (!stepParsing.classList.contains('complete')) {401                setStepComplete(stepParsing, indParsing, pbParsing, 'Done');402            }403            404            setStepActive(stepEmbedding, indEmbedding);405            pbEmbedding.style.width = progress + '%';406            407            if (data.status === 'complete') {408                setStepComplete(stepEmbedding, indEmbedding, pbEmbedding, 'Complete');409            } else {410                metaEmbedding.textContent = `Embedding: ${data.current} / ${data.total} (${progress}%)`;411            }412        } 413        else if (stage === 'upserting') {414            // Guarantee previous steps are closed out415            if (!stepParsing.classList.contains('complete')) {416                setStepComplete(stepParsing, indParsing, pbParsing, 'Done');417            }418            if (!stepEmbedding.classList.contains('complete')) {419                setStepComplete(stepEmbedding, indEmbedding, pbEmbedding, 'Done');420            }421            422            setStepActive(stepIngestion, indIngestion);423            pbIngestion.style.width = progress + '%';424            425            if (data.status === 'complete') {426                setStepComplete(stepIngestion, indIngestion, pbIngestion, 'Complete');427            } else {428                if (data.status === 'pending') {429                    metaIngestion.textContent = 'Enqueuing...';430                } else {431                    metaIngestion.textContent = `Upserting: ${data.current} / ${data.total} (${progress}%)`;432                }433            }434        }435    }436 437    // Stepper Helper Functions438    function resetSteps() {439        const steps = [stepParsing, stepEmbedding, stepIngestion];440        const indicators = [indParsing, indEmbedding, indIngestion];441        const pbs = [pbParsing, pbEmbedding, pbIngestion];442        const metas = [metaParsing, metaEmbedding, metaIngestion];443        444        steps.forEach(step => step.className = 'step-item');445        indicators.forEach((ind, i) => {446            ind.innerHTML = (i + 1).toString();447        });448        pbs.forEach(pb => pb.style.width = '0%');449        metas.forEach(meta => meta.textContent = 'Waiting...');450    }451 452    function setStepActive(stepElement, indicatorElement) {453        if (!stepElement.classList.contains('active') && !stepElement.classList.contains('complete')) {454            stepElement.className = 'step-item active';455            indicatorElement.innerHTML = '<i class="ph ph-spinner step-spinner"></i>';456        }457    }458 459    // Custom tactical transitions460    function setStepComplete(stepElement, indicatorElement, pbElement, metaText) {461        stepElement.className = 'step-item complete';462        indicatorElement.innerHTML = '<i class="ph ph-check"></i>';463        if (pbElement) pbElement.style.width = '100%';464        465        const metaEl = stepElement.querySelector('.step-meta');466        if (metaEl && metaText) {467            metaEl.textContent = metaText;468        }469    }470 471    // Query Search Handler472    searchForm.addEventListener('submit', async (e) => {473        e.preventDefault();474        const queryVal = searchQuery.value.trim();475        if (!queryVal) return;476 477        btnSearch.disabled = true;478        btnSearch.innerHTML = '<i class="ph ph-spinner step-spinner"></i> Processing query...';479        resultsContainer.innerHTML = '';480        searchMeta.style.display = 'none';481 482        try {483            const response = await fetch(`/api/search?query=${encodeURIComponent(queryVal)}&top_k=5`);484            if (!response.ok) {485                const errData = await response.json();486                throw new Error(errData.detail || 'Search API failed.');487            }488 489            const data = await response.json();490            491            btnSearch.disabled = false;492            btnSearch.innerHTML = '<i class="ph ph-sparkle"></i> Search Index';493 494            if (data.results && data.results.length > 0) {495                resultsCount.textContent = data.results.length;496                searchMeta.style.display = 'flex';497                498                data.results.forEach((result, idx) => {499                    const score = result.score;500                    let badgeClass = 'low';501                    if (score > 0.8) badgeClass = 'high';502                    else if (score > 0.6) badgeClass = 'mid';503 504                    const card = document.createElement('div');505                    card.className = 'result-card';506                    card.style.animation = `form-enter 0.4s cubic-bezier(0.16, 1, 0.3, 1) ${idx * 0.05}s both`;507                    card.innerHTML = `508                        <div class="result-card-header">509                            <span class="result-filename">510                                <i class="ph ph-file-text"></i> ${result.metadata.filename || 'Unknown Document'}511                            </span>512                            <span class="score-badge ${badgeClass}">Relevance: ${(score * 100).toFixed(1)}%</span>513                        </div>514                        <div class="result-text">${highlightText(result.metadata.text || '', queryVal)}</div>515                    `;516                    resultsContainer.appendChild(card);517                });518                showToast('Query Completed', `Retrieved ${data.results.length} relevant segments from index.`, 'success');519            } else {520                resultsContainer.innerHTML = `521                    <div class="no-results">522                        <i class="ph ph-info no-results-icon"></i>523                        <p>${data.message || 'No matching document text chunks found in your database.'}</p>524                    </div>525                `;526                showToast('Query Completed', 'No matches found.', 'info');527            }528 529        } catch (error) {530            btnSearch.disabled = false;531            btnSearch.innerHTML = '<i class="ph ph-sparkle"></i> Search Index';532            resultsContainer.innerHTML = `533                <div class="no-results" style="color: var(--error);">534                    <i class="ph ph-warning no-results-icon"></i>535                    <p>Search failed: ${error.message}</p>536                </div>537            `;538            showToast('Search Failed', error.message, 'error');539        }540    });541 542    // Highlight query terms inside the matching text blocks543    function highlightText(text, query) {544        if (!query) return escapeHtml(text);545        546        // Stopwords to ignore from highlighting547        const stopwords = new Set([548            'the', 'and', 'a', 'of', 'in', 'to', 'for', 'is', 'on', 'that', 'this', 'with', 'as', 'at', 'by', 'an', 'be', 'or', 'are', 'it', 'from'549        ]);550        551        // Entity references to avoid highlight collisions (prevents corruption of e.g. &amp;)552        const entityNames = new Set(['amp', 'lt', 'gt', 'quot', 'apos']);553        554        const terms = query555            .toLowerCase()556            .replace(/[.,\/#!$%\^&\*;:{}=\-_`~()?]/g, "")557            .split(/\s+/)558            .filter(t => t.length > 2 && !stopwords.has(t) && !entityNames.has(t));559            560        let escapedText = escapeHtml(text);561        if (terms.length === 0) return escapedText;562        563        // Sort by length desc to highlight longer match sequences first564        terms.sort((a, b) => b.length - a.length);565        566        // Escape terms for Regex matching567        const escapedTerms = terms.map(t => t.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'));568        569        const regex = new RegExp(`(${escapedTerms.join('|')})`, 'gi');570        return escapedText.replace(regex, '<mark class="matched-term">$1</mark>');571    }572 573    // Helper to safely escape HTML output574    function escapeHtml(text) {575        return text576            .replace(/&/g, "&amp;")577            .replace(/</g, "&lt;")578            .replace(/>/g, "&gt;")579            .replace(/"/g, "&quot;")580            .replace(/'/g, "&#039;");581    }582});583