agentpixel/pinecone-knowledge-indexer
0
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. &)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, "&")577 .replace(/</g, "<")578 .replace(/>/g, ">")579 .replace(/"/g, """)580 .replace(/'/g, "'");581 }582});583 