Spaces:
Running
Running
| // Final Corrected Version: June 13, 2025 | |
| document.addEventListener('DOMContentLoaded', () => { | |
| // --- CONFIGURATION --- | |
| const config = { | |
| backendUrl: 'https://ar-city-explorer-backend.aiagents.workers.dev' | |
| }; | |
| // --- GLOBAL STATE --- | |
| let poisData = []; | |
| let arActive = false; | |
| let userSettings = { | |
| showHistorical: true, showMenus: true, showNavigation: false, voiceResponses: true | |
| }; | |
| // --- DOM ELEMENT SELECTORS --- | |
| const arToggle = document.getElementById('arToggle'); | |
| const arViewport = document.getElementById('arViewport'); | |
| const normalView = document.getElementById('normalView'); | |
| const aiAssistant = document.getElementById('aiAssistant'); | |
| const userInput = document.getElementById('userInput'); | |
| const sendBtn = document.getElementById('sendBtn'); | |
| const objectModal = document.getElementById('objectModal'); | |
| const objectTitle = document.getElementById('objectTitle'); | |
| const objectDescription = document.getElementById('objectDescription'); | |
| const objectImage = document.getElementById('objectImage'); | |
| const closeObjectModal = document.getElementById('closeObjectModal'); | |
| const settingsBtn = document.getElementById('settingsBtn'); | |
| const settingsModal = document.getElementById('settingsModal'); | |
| const closeSettingsModal = document.getElementById('closeSettingsModal'); | |
| const aerodeckSection = document.getElementById('aerodeckSection'); | |
| const aerodeckList = document.getElementById('aerodeckList'); | |
| // Add these lines to the list of selectors | |
| const glassesBtn = document.getElementById('glassesBtn'); | |
| const glassesModal = document.getElementById('glassesModal'); | |
| const closeGlassesModal = document.getElementById('closeGlassesModal'); | |
| // --- FUNCTIONS --- | |
| function toggleARView(showAR) { | |
| arActive = showAR; | |
| if (arActive) { | |
| normalView.classList.add('hidden'); | |
| arViewport.classList.remove('hidden'); | |
| arToggle.innerHTML = '<i class="fas fa-times mr-1"></i> Exit AR'; | |
| if (poisData.length === 0) fetchPoisAndCreateAREntities(); | |
| } else { | |
| arViewport.classList.add('hidden'); | |
| normalView.classList.remove('hidden'); | |
| arToggle.innerHTML = '<i class="fas fa-vr-cardboard mr-1"></i> AR Mode'; | |
| } | |
| } | |
| function fetchPoisAndCreateAREntities() { | |
| fetch(`${config.backendUrl}/api/pois`) | |
| .then(response => { if (!response.ok) throw new Error('Network error'); return response.json(); }) | |
| .then(pois => { | |
| poisData = pois; | |
| const scene = document.querySelector('a-scene'); | |
| if (!scene) return; | |
| pois.forEach(poi => { | |
| const entity = document.createElement('a-entity'); | |
| entity.setAttribute('gps-new-entity-place', { latitude: poi.latitude, longitude: poi.longitude }); | |
| const box = document.createElement('a-box'); | |
| box.setAttribute('material', 'color: red; opacity: 0.7;'); | |
| box.setAttribute('scale', '10 10 10'); | |
| box.setAttribute('position', '0 5 0'); | |
| box.setAttribute('data-poi-id', poi.id); | |
| entity.appendChild(box); | |
| const text = document.createElement('a-text'); | |
| text.setAttribute('value', poi.name); | |
| text.setAttribute('look-at', '[gps-new-camera]'); | |
| text.setAttribute('scale', '50 50 50'); | |
| text.setAttribute('position', '0 15 0'); | |
| entity.appendChild(text); | |
| scene.appendChild(entity); | |
| }); | |
| }) | |
| .catch(error => { | |
| console.error('Failed to load POIs:', error); | |
| alert('Could not load city data. Check connection and backend URL.'); | |
| toggleARView(false); | |
| }); | |
| } | |
| function showObjectInfo(poiId) { | |
| objectTitle.textContent = 'Loading...'; | |
| objectDescription.textContent = ''; | |
| if (aerodeckSection) aerodeckSection.classList.add('hidden'); | |
| fetch(`${config.backendUrl}/api/pois/${poiId}`) | |
| .then(response => { if (!response.ok) throw new Error('POI not found'); return response.json(); }) | |
| .then(data => { | |
| objectTitle.textContent = data.name; | |
| objectDescription.textContent = data.description || "No description available."; | |
| objectImage.src = `https://via.placeholder.com/300x200?text=${encodeURIComponent(data.name)}`; | |
| if (data.aerodecks && data.aerodecks.length > 0) { | |
| aerodeckList.innerHTML = ''; | |
| data.aerodecks.forEach(deck => { | |
| const itemElement = document.createElement('div'); | |
| itemElement.className = 'flex justify-between items-center p-2 bg-gray-100 rounded'; | |
| const statusColor = deck.status === 'Operational' ? 'text-green-500' : 'text-orange-500'; | |
| itemElement.innerHTML = `<div><div class="font-medium">${deck.deck_name}</div><div class="text-sm text-gray-600">Size: ${deck.size_meters}m | Charging: ${deck.is_charging_available ? 'Yes' : 'No'}</div></div><div class="font-bold text-sm ${statusColor}">${deck.status}</div>`; | |
| aerodeckList.appendChild(itemElement); | |
| }); | |
| if (aerodeckSection) aerodeckSection.classList.remove('hidden'); | |
| } | |
| if (objectModal) objectModal.classList.remove('hidden'); | |
| }) | |
| .catch(error => { console.error("Error fetching POI details:", error); alert("Could not load details for this location."); }); | |
| } | |
| function addUserMessage(message) { | |
| const chatContainer = aiAssistant.querySelector('.flex-col'); | |
| const msg = `<div class="ai-message user-message"><p>${message}</p></div>`; | |
| chatContainer.insertAdjacentHTML('beforeend', msg); | |
| aiAssistant.scrollTop = aiAssistant.scrollHeight; | |
| } | |
| function addAIMessage(message) { | |
| const chatContainer = aiAssistant.querySelector('.flex-col'); | |
| const msg = `<div class="ai-message assistant-message"><div class="font-bold text-indigo-800 mb-1">AR Guide</div><p>${message}</p></div>`; | |
| chatContainer.insertAdjacentHTML('beforeend', msg); | |
| aiAssistant.scrollTop = aiAssistant.scrollHeight; | |
| } | |
| function handleSearch() { | |
| const searchTerm = userInput.value.trim(); | |
| if (!searchTerm) return; | |
| addUserMessage(searchTerm); | |
| userInput.value = ''; | |
| const searchKeywords = ['search', 'find', 'show', 'where is', 'landmark', 'park', 'beach', 'hollywood', 'map', 'navigate']; | |
| const isLocalSearch = searchKeywords.some(keyword => searchTerm.toLowerCase().includes(keyword)); | |
| if (isLocalSearch) { | |
| const stopWords = ['show', 'me', 'find', 'is', 'a', 'the', 'for', 'where', 'search', 'of', 'at']; | |
| const searchTokens = searchTerm.toLowerCase().split(' ').filter(word => !stopWords.includes(word) && word.length > 2); | |
| if (searchTokens.length === 0) return addAIMessage("Please be more specific in your local search."); | |
| const results = poisData.filter(poi => { | |
| const poiText = (poi.name + ' ' + (poi.description || '')).toLowerCase(); | |
| return searchTokens.some(token => { | |
| if (poiText.includes(token)) return true; | |
| if (token.endsWith('s')) return poiText.includes(token.slice(0, -1)); | |
| return false; | |
| }); | |
| }); | |
| if (results.length > 0) { | |
| let responseMessage = `I found ${results.length} local result(s):<ul>`; | |
| results.forEach(poi => { responseMessage += `<li class="mt-2 list-disc list-inside">${poi.name}</li>`; }); | |
| responseMessage += "</ul>"; | |
| addAIMessage(responseMessage); | |
| } else { | |
| addAIMessage(`Sorry, I couldn't find any local places matching "${searchTerm}".`); | |
| } | |
| } else { | |
| addAIMessage("AI is thinking..."); | |
| fetch(`${config.backendUrl}/api/ask`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ prompt: searchTerm }) | |
| }) | |
| .then(response => { if (!response.ok) throw new Error('Network error'); return response.json(); }) | |
| .then(data => { | |
| const chatContainer = aiAssistant.querySelector('.flex-col'); | |
| const thinkingMessage = Array.from(chatContainer.querySelectorAll('.assistant-message')).pop(); | |
| if (thinkingMessage && thinkingMessage.textContent.includes("AI is thinking...")) { | |
| thinkingMessage.querySelector('p').innerHTML = data.response.replace(/\n/g, '<br>'); | |
| } else { | |
| addAIMessage(data.response.replace(/\n/g, '<br>')); | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('Error asking AI:', error); | |
| const chatContainer = aiAssistant.querySelector('.flex-col'); | |
| const thinkingMessage = Array.from(chatContainer.querySelectorAll('.assistant-message')).pop(); | |
| if (thinkingMessage && thinkingMessage.textContent.includes("AI is thinking...")) { | |
| thinkingMessage.querySelector('p').innerHTML = "Sorry, I had trouble connecting to the AI."; | |
| } else { | |
| addAIMessage("Sorry, I had trouble connecting to the AI."); | |
| } | |
| }); | |
| } | |
| } | |
| // --- EVENT LISTENERS (Corrected Version) --- | |
| // AR Mode Toggle | |
| if(arToggle) arToggle.addEventListener('click', () => toggleARView(!arActive)); | |
| // Clicking on AR objects in the scene | |
| const sceneEl = document.querySelector('a-scene'); | |
| if (sceneEl) { | |
| sceneEl.addEventListener('click', (event) => { | |
| if (event.target.hasAttribute('data-poi-id')) { | |
| const poiId = parseInt(event.target.getAttribute('data-poi-id'), 10); | |
| showObjectInfo(poiId); | |
| } | |
| }); | |
| } | |
| // Info Modal Close Button | |
| if(closeObjectModal) closeObjectModal.addEventListener('click', () => objectModal.classList.add('hidden')); | |
| // Search Bar Send Button & Enter Key | |
| if(sendBtn) sendBtn.addEventListener('click', handleSearch); | |
| if(userInput) userInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') handleSearch(); }); | |
| // Settings Modal Open/Close Logic | |
| if(settingsBtn) { | |
| settingsBtn.addEventListener('click', () => { | |
| if(settingsModal) settingsModal.classList.remove('hidden'); | |
| }); | |
| } | |
| if(closeSettingsModal) { | |
| closeSettingsModal.addEventListener('click', () => { | |
| if(settingsModal) settingsModal.classList.add('hidden'); | |
| }); | |
| } | |
| if(settingsModal) { | |
| settingsModal.addEventListener('click', (event) => { | |
| if (event.target === settingsModal) settingsModal.classList.add('hidden'); | |
| }); | |
| } | |
| // Glasses Modal Open/Close Logic | |
| if (glassesBtn) { | |
| glassesBtn.addEventListener('click', () => { | |
| if(glassesModal) glassesModal.classList.remove('hidden'); | |
| }); | |
| } | |
| if (closeGlassesModal) { | |
| closeGlassesModal.addEventListener('click', () => { | |
| if(glassesModal) glassesModal.classList.add('hidden'); | |
| }); | |
| } | |
| if (glassesModal) { | |
| glassesModal.addEventListener('click', (event) => { | |
| if (event.target === glassesModal) glassesModal.classList.add('hidden'); | |
| }); | |
| } | |
| // Settings Toggles Functionality | |
| const settingToggles = settingsModal ? settingsModal.querySelectorAll('input[data-setting]') : []; | |
| settingToggles.forEach(toggle => { | |
| const settingName = toggle.dataset.setting; | |
| if (userSettings[settingName] !== undefined) { toggle.checked = userSettings[settingName]; } | |
| toggle.addEventListener('change', (event) => { | |
| const changedSettingName = event.target.dataset.setting; | |
| userSettings[changedSettingName] = event.target.checked; | |
| // console.log('Settings updated:', userSettings); // For testing | |
| }); | |
| }); | |
| // --- INITIALIZATION --- | |
| fetchPoisAndCreateAREntities(); | |
| }); |