Download script.js from nasa-hackathon1/bio_search_engine_hf: direct link, hf CLI and curl.
- Browser
- Download file 18 kB
-
https://huggingface.co/spaces/nasa-hackathon1/bio_search_engine_hf/resolve/main/script.js
- Command line
-
hf download hf://spaces/nasa-hackathon1/bio_search_engine_hf/script.js
-
curl -L -o script.js https://huggingface.co/spaces/nasa-hackathon1/bio_search_engine_hf/resolve/main/script.js
18 kB
| document.addEventListener('DOMContentLoaded', () => { | |
| const API_URL = ''; // Use relative path for API calls | |
| // --- Application Elements --- | |
| const nasaSearchInput = document.getElementById('nasaSearchInput'); | |
| const nasaSearchBtn = document.getElementById('nasaSearchBtn'); | |
| const searchResultsList = document.getElementById('search-results-list'); | |
| const trainChatbotBtn = document.getElementById('trainChatbotBtn'); | |
| const chatHistory = document.getElementById('chat-history'); | |
| const chatInput = document.getElementById('chat-input'); | |
| const chatSendBtn = document.getElementById('chat-send'); | |
| // --- Application State --- | |
| let currentSessionId = null; | |
| // --- Helper Functions --- | |
| function addChatMessage(sender, message, sources = []) { | |
| const messageElement = document.createElement('div'); | |
| messageElement.classList.add('chat-message', sender.toLowerCase()); | |
| messageElement.style.marginBottom = '12px'; | |
| // Use innerText for safety, but replace newlines with <br> for rendering | |
| const formattedMessage = message.replace(/\n/g, '<br>'); | |
| let sourcesHTML = ''; | |
| if (sources.length > 0) { | |
| sourcesHTML = '<div class="sources" style="margin-top: 10px; font-size: 0.8rem; opacity: 0.7;"><strong>Sources:</strong><ul>'; | |
| sources.forEach(source => { | |
| sourcesHTML += `<li style="margin-left: 20px;">${source}</li>`; | |
| }); | |
| sourcesHTML += '</ul></div>'; | |
| } | |
| messageElement.innerHTML = ` | |
| <strong style="display: block; margin-bottom: 5px;">${sender}:</strong> | |
| <span>${formattedMessage}</span> | |
| ${sourcesHTML} | |
| `; | |
| chatHistory.appendChild(messageElement); | |
| chatHistory.scrollTop = chatHistory.scrollHeight; | |
| } | |
| // --- Event Listeners --- | |
| // 1. Search NASA Papers | |
| nasaSearchBtn.addEventListener('click', async () => { | |
| const keyword = nasaSearchInput.value.trim(); | |
| if (!keyword) { | |
| alert('Please enter a search keyword.'); | |
| return; | |
| } | |
| searchResultsList.innerHTML = '<p>Searching...</p>'; | |
| trainChatbotBtn.style.display = 'none'; | |
| nasaSearchBtn.disabled = true; | |
| nasaSearchBtn.textContent = 'Searching...'; | |
| try { | |
| const response = await fetch(`${API_URL}/api/search`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ keyword }), | |
| }); | |
| if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); | |
| const results = await response.json(); | |
| displaySearchResults(results); | |
| } catch (error) { | |
| console.error('Search failed:', error); | |
| searchResultsList.innerHTML = `<p style="color: #ff5e00;">Search failed. Please check the console or try again.</p>`; | |
| } finally { | |
| nasaSearchBtn.disabled = false; | |
| nasaSearchBtn.textContent = 'Search'; | |
| } | |
| }); | |
| // 2. Display Search Results | |
| function displaySearchResults(results) { | |
| searchResultsList.innerHTML = ''; | |
| if (results.length === 0) { | |
| searchResultsList.innerHTML = '<p>No papers with downloadable PDFs found for this keyword.</p>'; | |
| return; | |
| } | |
| results.forEach(paper => { | |
| const paperElement = document.createElement('div'); | |
| paperElement.classList.add('paper-item'); | |
| paperElement.style.marginBottom = '15px'; | |
| paperElement.innerHTML = ` | |
| <label style="display: flex; align-items: flex-start; cursor: pointer;"> | |
| <input type="checkbox" class="paper-checkbox" data-pdf-url="${paper.pdfLink}" data-paper-id="${paper.id}" style="margin-top: 5px; margin-right: 10px;"> | |
| <div> | |
| <strong style="color: #fff;">${paper.title}</strong> | |
| <p style="font-size: 0.9rem; opacity: 0.7; margin-top: 5px;">${paper.abstract ? paper.abstract.substring(0, 150) + '...' : 'No abstract available.'}</p> | |
| </div> | |
| </label> | |
| `; | |
| searchResultsList.appendChild(paperElement); | |
| }); | |
| trainChatbotBtn.style.display = 'block'; | |
| } | |
| // 3a. Poll for training status | |
| function pollTrainingStatus(jobId) { | |
| const interval = setInterval(async () => { | |
| try { | |
| const response = await fetch(`${API_URL}/api/train/status/${jobId}`); | |
| // Gracefully handle server restarts where the job is lost | |
| if (response.status === 404) { | |
| clearInterval(interval); | |
| throw new Error("Training process was interrupted on the server. Please try again."); | |
| } | |
| const result = await response.json(); | |
| if (result.status === 'processing' || result.status === 'starting') { | |
| addChatMessage('System', result.message); | |
| } else if (result.status === 'complete') { | |
| clearInterval(interval); | |
| currentSessionId = result.sessionId; | |
| addChatMessage('Bot', result.message + " You can now ask me questions about them."); | |
| trainChatbotBtn.disabled = false; | |
| trainChatbotBtn.textContent = 'Train Chatbot on Selected Papers'; | |
| } else if (result.status === 'error') { | |
| clearInterval(interval); | |
| throw new Error(result.message || 'Training job failed on the server.'); | |
| } | |
| } catch (error) { | |
| clearInterval(interval); | |
| console.error('Polling failed:', error); | |
| addChatMessage('System', `Error during training: ${error.message}`); | |
| trainChatbotBtn.disabled = false; | |
| trainChatbotBtn.textContent = 'Train Chatbot on Selected Papers'; | |
| } | |
| }, 3000); // Check every 3 seconds | |
| } | |
| // 3b. Display selected papers and hide search results | |
| function showSelectedPapers(papers) { | |
| const searchPanel = document.getElementById('search-panel'); | |
| const selectedPapersPanel = document.getElementById('selected-papers-panel'); | |
| const selectedPapersList = document.getElementById('selected-papers-list'); | |
| selectedPapersList.innerHTML = ''; // Clear previous list | |
| papers.forEach(paper => { | |
| const paperElement = document.createElement('div'); | |
| paperElement.innerHTML = ` | |
| <a href="${paper.url}" target="_blank" rel="noopener noreferrer" class="selected-paper-link" style="display: block; padding: 10px; border-radius: 5px; text-decoration: none; color: #fff; background: rgba(255,255,255,0.05); margin-bottom: 10px; transition: background 0.3s;"> | |
| ${paper.title} | |
| </a> | |
| `; | |
| selectedPapersList.appendChild(paperElement); | |
| }); | |
| searchPanel.style.display = 'none'; | |
| selectedPapersPanel.style.display = 'block'; | |
| } | |
| // 3. Train Chatbot | |
| trainChatbotBtn.addEventListener('click', async () => { | |
| const selectedCheckboxes = document.querySelectorAll('.paper-checkbox:checked'); | |
| const papers = Array.from(selectedCheckboxes).map(cb => ({ | |
| id: cb.dataset.paperId, | |
| url: cb.dataset.pdfUrl, | |
| title: cb.closest('.paper-item').querySelector('strong').textContent, | |
| })); | |
| if (papers.length === 0) { | |
| alert('Please select at least one paper to train the chatbot.'); | |
| return; | |
| } | |
| addChatMessage('System', `Training chatbot on ${papers.length} paper(s)... This may take a moment.`); | |
| trainChatbotBtn.disabled = true; | |
| trainChatbotBtn.textContent = 'Training...'; | |
| // Show the selected papers list and hide the search results | |
| showSelectedPapers(papers); | |
| // Activate the three-column layout for the knowledge map | |
| document.getElementById('main-app-container').classList.add('knowledge-map-active'); | |
| try { | |
| const response = await fetch(`${API_URL}/api/train`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ papers }), | |
| }); | |
| const result = await response.json(); | |
| if (response.status === 202) { // 202 Accepted | |
| addChatMessage('System', result.message); | |
| pollTrainingStatus(result.jobId); // Start polling for status | |
| } else { | |
| throw new Error(result.error || 'Failed to start training job.'); | |
| } | |
| } catch (error) { | |
| console.error('Training failed:', error); | |
| addChatMessage('System', `Error during training: ${error.message}`); | |
| } | |
| }); | |
| // 4. Handle Chat Messages | |
| async function handleSendMessage() { | |
| const query = chatInput.value.trim(); | |
| if (!query) return; | |
| if (!currentSessionId) { | |
| addChatMessage('System', 'Error: No active training session. Please train the chatbot first.'); | |
| return; | |
| } | |
| addChatMessage('You', query); | |
| chatInput.value = ''; | |
| chatSendBtn.disabled = true; | |
| try { | |
| const response = await fetch(`${API_URL}/api/ask`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ query, sessionId: currentSessionId }), | |
| }); | |
| const result = await response.json(); | |
| if (!response.ok) throw new Error(result.error || 'Failed to get an answer.'); | |
| addChatMessage('Bot', result.answer, result.sources); | |
| } catch (error) { | |
| console.error('Ask failed:', error); | |
| addChatMessage('System', `Error: ${error.message}`); | |
| } finally { | |
| chatSendBtn.disabled = false; | |
| } | |
| } | |
| chatSendBtn.addEventListener('click', handleSendMessage); | |
| chatInput.addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter') { | |
| handleSendMessage(); | |
| } | |
| }); | |
| // --- Original Theme JavaScript --- | |
| // (The existing code for particles, menu, text rotation, etc., goes here) | |
| // Create floating particles | |
| function createParticles() { | |
| const particlesContainer = document.getElementById('particles'); | |
| const particleCount = 30; | |
| for (let i = 0; i < particleCount; i++) { | |
| const particle = document.createElement('div'); | |
| particle.className = 'particle'; | |
| particle.style.left = Math.random() * 100 + '%'; | |
| particle.style.animationDelay = Math.random() * 15 + 's'; | |
| particle.style.animationDuration = (Math.random() * 10 + 15) + 's'; | |
| // Randomly assign orange or blue color | |
| if (Math.random() > 0.5) { | |
| particle.style.setProperty('--particle-color', '#00B2FF'); | |
| const before = particle.style.getPropertyValue('--particle-color'); | |
| particle.style.background = '#00B2FF'; | |
| } | |
| particlesContainer.appendChild(particle); | |
| } | |
| } | |
| // Mobile menu toggle | |
| const menuToggle = document.getElementById('menuToggle'); | |
| const navLinks = document.getElementById('navLinks'); | |
| menuToggle.addEventListener('click', () => { | |
| menuToggle.classList.toggle('active'); | |
| navLinks.classList.toggle('active'); | |
| }); | |
| // Close mobile menu when clicking a link | |
| document.querySelectorAll('.nav-links a').forEach(link => { | |
| link.addEventListener('click', () => { | |
| menuToggle.classList.remove('active'); | |
| navLinks.classList.remove('active'); | |
| }); | |
| }); | |
| // Active navigation highlighting | |
| const sections = document.querySelectorAll('section'); | |
| const navItems = document.querySelectorAll('.nav-link'); | |
| function updateActiveNav() { | |
| const scrollPosition = window.pageYOffset + 100; | |
| sections.forEach((section, index) => { | |
| const sectionTop = section.offsetTop; | |
| const sectionHeight = section.offsetHeight; | |
| if (scrollPosition >= sectionTop && scrollPosition < sectionTop + sectionHeight) { | |
| navItems.forEach(item => item.classList.remove('active')); | |
| const currentNav = document.querySelector(`.nav-link[href="#${section.id}"]`); | |
| if (currentNav) currentNav.classList.add('active'); | |
| } | |
| }); | |
| } | |
| // Navbar scroll effect | |
| window.addEventListener('scroll', function() { | |
| const navbar = document.getElementById('navbar'); | |
| if (window.scrollY > 50) { | |
| navbar.classList.add('scrolled'); | |
| } else { | |
| navbar.classList.remove('scrolled'); | |
| } | |
| updateActiveNav(); | |
| }); | |
| // Initial active nav update | |
| updateActiveNav(); | |
| // Smooth scrolling for navigation links | |
| document.querySelectorAll('a[href^="#"]').forEach(anchor => { | |
| anchor.addEventListener('click', function (e) { | |
| e.preventDefault(); | |
| const target = document.querySelector(this.getAttribute('href')); | |
| if (target) { | |
| target.scrollIntoView({ | |
| behavior: 'smooth', | |
| block: 'start' | |
| }); | |
| } | |
| }); | |
| }); | |
| // Feature tabs functionality | |
| const tabs = document.querySelectorAll('.tab-item'); | |
| const panels = document.querySelectorAll('.content-panel'); | |
| tabs.forEach(tab => { | |
| tab.addEventListener('click', () => { | |
| const tabId = tab.getAttribute('data-tab'); | |
| // Remove active class from all tabs and panels | |
| tabs.forEach(t => t.classList.remove('active')); | |
| panels.forEach(p => p.classList.remove('active')); | |
| // Add active class to clicked tab and corresponding panel | |
| tab.classList.add('active'); | |
| document.getElementById(tabId).classList.add('active'); | |
| }); | |
| }); | |
| // Form submission | |
| document.getElementById('contactForm').addEventListener('submit', function(e) { | |
| e.preventDefault(); | |
| // Add your form submission logic here | |
| alert('Message sent! We\'ll get back to you soon.'); | |
| this.reset(); | |
| }); | |
| // Initialize particles | |
| createParticles(); | |
| // Text rotation with character animation | |
| const textSets = document.querySelectorAll('.text-set'); | |
| let currentIndex = 0; | |
| let isAnimating = false; | |
| function wrapTextInSpans(element) { | |
| const text = element.textContent; | |
| element.innerHTML = text.split('').map((char, i) => | |
| `<span class="char" style="animation-delay: ${i * 0.05}s">${char === ' ' ? ' ' : char}</span>` | |
| ).join(''); | |
| } | |
| function animateTextIn(textSet) { | |
| const glitchText = textSet.querySelector('.glitch-text'); | |
| const subtitle = textSet.querySelector('.subtitle'); | |
| // Wrap text in spans for animation | |
| wrapTextInSpans(glitchText); | |
| // Update data attribute for glitch effect | |
| glitchText.setAttribute('data-text', glitchText.textContent); | |
| // Show subtitle after main text | |
| setTimeout(() => { | |
| subtitle.classList.add('visible'); | |
| }, 800); | |
| } | |
| function animateTextOut(textSet) { | |
| const chars = textSet.querySelectorAll('.char'); | |
| const subtitle = textSet.querySelector('.subtitle'); | |
| // Animate characters out | |
| chars.forEach((char, i) => { | |
| char.style.animationDelay = `${i * 0.02}s`; | |
| char.classList.add('out'); | |
| }); | |
| // Hide subtitle | |
| subtitle.classList.remove('visible'); | |
| } | |
| function rotateText() { | |
| if (isAnimating) return; | |
| isAnimating = true; | |
| const currentSet = textSets[currentIndex]; | |
| const nextIndex = (currentIndex + 1) % textSets.length; | |
| const nextSet = textSets[nextIndex]; | |
| // Animate out current text | |
| animateTextOut(currentSet); | |
| // After out animation, switch sets | |
| setTimeout(() => { | |
| currentSet.classList.remove('active'); | |
| nextSet.classList.add('active'); | |
| animateTextIn(nextSet); | |
| currentIndex = nextIndex; | |
| isAnimating = false; | |
| }, 600); | |
| } | |
| // Initialize first text set | |
| textSets[0].classList.add('active'); | |
| animateTextIn(textSets[0]); | |
| // Start rotation after initial display | |
| setTimeout(() => { | |
| setInterval(rotateText, 5000); // Change every 5 seconds | |
| }, 4000); | |
| // Add random glitch effect | |
| setInterval(() => { | |
| const glitchTexts = document.querySelectorAll('.glitch-text'); | |
| glitchTexts.forEach(text => { | |
| if (Math.random() > 0.95) { | |
| text.style.animation = 'none'; | |
| setTimeout(() => { | |
| text.style.animation = ''; | |
| }, 200); | |
| } | |
| }); | |
| }, 3000); | |
| }); |