// ============================================ // PASSWORD GATEKEEPER // ============================================ const DEMO_PASSWORD = 'agri2026'; function initPasswordGatekeeper() { const overlay = document.getElementById('passwordOverlay'); const mainContent = document.getElementById('mainContent'); const submitBtn = document.getElementById('submitPasswordBtn'); const passwordInput = document.getElementById('passwordInput'); const errorDiv = document.getElementById('passwordError'); if (sessionStorage.getItem('demo_unlocked') === 'true') { if (overlay) overlay.style.display = 'none'; if (mainContent) mainContent.style.display = 'block'; return; } if (overlay) overlay.style.display = 'flex'; if (mainContent) mainContent.style.display = 'none'; if (!submitBtn) return; function checkPassword() { if (passwordInput.value === DEMO_PASSWORD) { sessionStorage.setItem('demo_unlocked', 'true'); if (overlay) overlay.style.display = 'none'; if (mainContent) mainContent.style.display = 'block'; if (errorDiv) errorDiv.textContent = ''; } else { if (errorDiv) errorDiv.textContent = 'Wrong password. Please try again.'; if (passwordInput) passwordInput.value = ''; if (passwordInput) passwordInput.focus(); } } submitBtn.addEventListener('click', checkPassword); if (passwordInput) { passwordInput.addEventListener('keypress', function(e) { if (e.key === 'Enter') checkPassword(); }); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initPasswordGatekeeper); } else { initPasswordGatekeeper(); } // ============================================ // ============================================ // ACCORDION TOGGLE FUNCTIONALITY // ============================================ function initAccordions() { const accordionHeaders = document.querySelectorAll('.accordion-header'); const storageKey = 'accordion_states'; let savedStates = {}; try { savedStates = JSON.parse(localStorage.getItem(storageKey)) || {}; } catch(e) {} accordionHeaders.forEach(header => { const accordionId = header.getAttribute('data-accordion'); const module = header.closest('.accordion-module'); if (savedStates[accordionId] === 'collapsed') { module.classList.add('collapsed'); } else { module.classList.remove('collapsed'); } }); accordionHeaders.forEach(header => { header.addEventListener('click', function() { const module = this.closest('.accordion-module'); const accordionId = this.getAttribute('data-accordion'); module.classList.toggle('collapsed'); const isCollapsed = module.classList.contains('collapsed'); savedStates[accordionId] = isCollapsed ? 'collapsed' : 'expanded'; localStorage.setItem(storageKey, JSON.stringify(savedStates)); }); }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initAccordions); } else { initAccordions(); } // ============================================ // ============================================ // MOBILE SIDEBAR TOGGLE // ============================================ function initMobileSidebar() { const menuToggle = document.getElementById('menuToggle'); const sidebar = document.getElementById('sidebarPanel'); const closeSidebarBtn = document.getElementById('closeSidebarBtn'); const overlay = document.getElementById('mobileSidebarOverlay'); if (!menuToggle) return; function openSidebar() { if (sidebar) sidebar.classList.add('open'); if (overlay) overlay.style.display = 'block'; document.body.style.overflow = 'hidden'; } function closeSidebar() { if (sidebar) sidebar.classList.remove('open'); if (overlay) overlay.style.display = 'none'; document.body.style.overflow = ''; } menuToggle.addEventListener('click', openSidebar); if (closeSidebarBtn) { closeSidebarBtn.addEventListener('click', closeSidebar); } if (overlay) { overlay.addEventListener('click', closeSidebar); } window.addEventListener('resize', function() { if (window.innerWidth > 768) { closeSidebar(); } }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initMobileSidebar); } else { initMobileSidebar(); } // ============================================ // ============================================ // WEATHER LOCATION MODE TOGGLE // ============================================ function initWeatherLocationMode() { const cityModeBtn = document.getElementById('cityModeBtn'); const coordModeBtn = document.getElementById('coordModeBtn'); const cityModeDiv = document.getElementById('cityMode'); const coordModeDiv = document.getElementById('coordMode'); const getForecastByCoordBtn = document.getElementById('getForecastByCoordBtn'); if (!cityModeBtn || !coordModeBtn) return; cityModeBtn.addEventListener('click', function() { cityModeBtn.classList.add('active'); coordModeBtn.classList.remove('active'); cityModeDiv.style.display = 'block'; coordModeDiv.style.display = 'none'; }); coordModeBtn.addEventListener('click', function() { coordModeBtn.classList.add('active'); cityModeBtn.classList.remove('active'); cityModeDiv.style.display = 'none'; coordModeDiv.style.display = 'block'; }); if (getForecastByCoordBtn) { getForecastByCoordBtn.addEventListener('click', function() { const lat = document.getElementById('latitude').value; const lon = document.getElementById('longitude').value; if (!lat || !lon) { alert('Please enter both latitude and longitude'); return; } updateForecastWidgetByCoords(parseFloat(lat), parseFloat(lon)); }); } } async function updateForecastWidgetByCoords(lat, lon) { const forecastCards = document.getElementById('forecastCards'); const weatherWidget = document.getElementById('weatherWidget'); if (!forecastCards) return; forecastCards.innerHTML = '
Loading forecast...
'; try { const response = await fetch(`/api/weather/forecast?lat=${lat}&lon=${lon}`); if (!response.ok) throw new Error('Weather fetch failed'); const data = await response.json(); if (data.forecast && data.forecast.length > 0) { const firstDay = data.forecast[0]; updateWeatherSummaryInAccordion(firstDay.condition, Math.round(firstDay.temp_max), ''); } let cardsHtml = ''; for (const day of data.forecast) { const icon = getWeatherIcon(day.condition); cardsHtml += `
${day.day.substring(0, 3)}
${icon}
${Math.round(day.temp_max)}°
${day.rain_chance}%
`; } forecastCards.innerHTML = cardsHtml; if (weatherWidget) weatherWidget.style.display = 'block'; } catch (error) { console.error('Weather widget error:', error); forecastCards.innerHTML = '
Error loading forecast
'; } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initWeatherLocationMode); } else { initWeatherLocationMode(); } // ============================================ // DOM Elements const sendBtn = document.getElementById('sendBtn'); const clearBtn = document.getElementById('clearBtn'); const questionInput = document.getElementById('question'); const chatBox = document.getElementById('chatBox'); const cropType = document.getElementById('cropType'); const bbchStage = document.getElementById('bbchStage'); // Send crop/BBCH only on first message or when the user explicitly changes them let cropDirty = true; let bbchDirty = true; const farmingType = document.getElementById('farmingType'); const phenologyStage = document.getElementById('phenologyStage'); const locationInput = document.getElementById('location'); const getForecastBtn = document.getElementById('getForecastBtn'); const soilMoisture = document.getElementById('soilMoisture'); const soilTemp = document.getElementById('soilTemp'); const airTemp = document.getElementById('airTemp'); const humidity = document.getElementById('humidity'); const imageInput = document.getElementById('imageInput'); const imagePreviewThumb = document.getElementById('imagePreviewThumb'); const thumbImg = document.getElementById('thumbImg'); const removeThumbBtn = document.getElementById('removeThumbBtn'); const weatherWidget = document.getElementById('weatherWidget'); const forecastCards = document.getElementById('forecastCards'); // Toggle elements (sensors and weather only - crop/bbch toggles REMOVED) const sensorsToggle = document.getElementById('sensorsToggle'); const weatherToggle = document.getElementById('weatherToggle'); const sensorsStatus = document.getElementById('sensorsStatus'); const weatherStatus = document.getElementById('weatherStatus'); // Always generate a fresh session ID on page load — ensures refresh = clean start function generateSessionId() { return 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); } let sessionId = generateSessionId(); console.log('Session ID:', sessionId); let attachedImageBase64 = null; // Crop and BBCH are ALWAYS enabled cropType.disabled = false; bbchStage.disabled = false; // Update toggle status display (sensors and weather only) function updateToggleStatus() { if (sensorsStatus) { const isOn = sensorsToggle.checked; sensorsStatus.textContent = isOn ? 'ON' : 'OFF'; sensorsStatus.className = isOn ? 'toggle-status active' : 'toggle-status'; } if (weatherStatus) { const isOn = weatherToggle.checked; weatherStatus.textContent = isOn ? 'ON' : 'OFF'; weatherStatus.className = isOn ? 'toggle-status active' : 'toggle-status'; } } // Load phenology stages for selected crop async function loadPhenologyStages(cropTypeValue) { // Skip loading phenology for "other" crop if (!cropTypeValue || cropTypeValue === 'other') { phenologyStage.innerHTML = ''; return; } try { const response = await fetch(`/api/phenology/stages/${cropTypeValue}`); if (!response.ok) throw new Error('Failed to load phenology data'); const data = await response.json(); const stages = data.stages; let options = ''; for (const stage of stages) { options += ``; } phenologyStage.innerHTML = options; } catch (error) { console.error('Error loading phenology:', error); phenologyStage.innerHTML = ''; } } // Initialize toggle event listeners (sensors and weather only) if (sensorsToggle) { sensorsToggle.addEventListener('change', updateToggleStatus); } if (weatherToggle) { weatherToggle.addEventListener('change', updateToggleStatus); } updateToggleStatus(); // Load phenology when crop type changes if (cropType) { cropType.addEventListener('change', function() { loadPhenologyStages(this.value); }); } // Auto-fill BBCH when phenology stage is selected if (phenologyStage) { phenologyStage.addEventListener('change', function() { const selectedOption = this.options[this.selectedIndex]; const bbchValue = selectedOption.getAttribute('data-bbch'); if (bbchValue && bbchStage) { bbchStage.value = bbchValue; } }); } // Typing animation function async function addMessageWithTyping(text, isUser = false) { if (isUser) { const messageDiv = document.createElement('div'); messageDiv.className = 'user-message'; messageDiv.textContent = text; chatBox.appendChild(messageDiv); chatBox.scrollTop = chatBox.scrollHeight; return; } const messageDiv = document.createElement('div'); messageDiv.className = 'bot-message'; messageDiv.innerHTML = ''; chatBox.appendChild(messageDiv); let i = 0; const speed = 15; function typeNextChar() { if (i < text.length) { let currentHtml = ''; let inBold = false; let inItalic = false; for (let k = 0; k <= i; k++) { const char = text[k]; if (char === '*' && text[k+1] === '*') { if (!inBold) { currentHtml += ''; inBold = true; } else { currentHtml += ''; inBold = false; } k++; } else if (char === '*') { if (!inItalic) { currentHtml += ''; inItalic = true; } else { currentHtml += ''; inItalic = false; } } else if (char === '\n') { currentHtml += '
'; } else { currentHtml += char; } } messageDiv.innerHTML = currentHtml + '|'; i++; setTimeout(typeNextChar, speed); } else { let finalHtml = messageDiv.innerHTML; finalHtml = finalHtml.replace('|', ''); messageDiv.innerHTML = finalHtml; chatBox.scrollTop = chatBox.scrollHeight; } } typeNextChar(); chatBox.scrollTop = chatBox.scrollHeight; } // Add message to chat with image support function addUserMessageWithImage(text, imageBase64) { const messageDiv = document.createElement('div'); messageDiv.className = 'user-message'; const textSpan = document.createElement('div'); textSpan.textContent = text; messageDiv.appendChild(textSpan); if (imageBase64) { const imgContainer = document.createElement('div'); imgContainer.className = 'user-message-image'; const img = document.createElement('img'); img.src = imageBase64; img.alt = 'Attached image'; imgContainer.appendChild(img); messageDiv.appendChild(imgContainer); } chatBox.appendChild(messageDiv); chatBox.scrollTop = chatBox.scrollHeight; } function addMessageStatic(text, isUser = false) { const messageDiv = document.createElement('div'); messageDiv.className = isUser ? 'user-message' : 'bot-message'; if (!isUser) { let html = text .replace(/\*\*(.*?)\*\*/g, '$1') .replace(/\*(.*?)\*/g, '$1') .replace(/\n/g, '
'); messageDiv.innerHTML = html; } else { messageDiv.textContent = text; } chatBox.appendChild(messageDiv); chatBox.scrollTop = chatBox.scrollHeight; } function showLoading() { const loading = document.createElement('div'); loading.className = 'bot-message'; loading.id = 'loading'; loading.textContent = '🤔 Thinking...'; chatBox.appendChild(loading); chatBox.scrollTop = chatBox.scrollHeight; } function hideLoading() { const loading = document.getElementById('loading'); if (loading) loading.remove(); } function updateWeatherSummaryInAccordion(condition, temp, windSpeed) { const weatherHeaderSpan = document.querySelector('.accordion-header[data-accordion="weather"] .accordion-title'); if (weatherHeaderSpan) { weatherHeaderSpan.innerHTML = `☁️ Weather (${temp}°C, ${condition})`; } } function getWeatherIcon(condition) { const cond = condition.toLowerCase(); if (cond.includes('thunder')) return '⛈️'; if (cond.includes('snow')) return '❄️'; if (cond.includes('rain') || cond.includes('shower') || cond.includes('drizzle')) return '🌧️'; if (cond.includes('fog') || cond.includes('mist')) return '🌫️'; if (cond.includes('overcast')) return '☁️'; if (cond.includes('cloud')) return '🌤️'; if (cond.includes('sun') || cond.includes('clear')) return '☀️'; return '🌡️'; } async function updateForecastWidget(location) { if (!location) { if (forecastCards) forecastCards.innerHTML = '
Enter a location and click "Get Forecast"
'; return; } try { const response = await fetch(`/api/weather/forecast?city=${encodeURIComponent(location)}`); if (!response.ok) throw new Error('Weather fetch failed'); const data = await response.json(); if (data.forecast && data.forecast.length > 0) { const firstDay = data.forecast[0]; updateWeatherSummaryInAccordion(firstDay.condition, Math.round(firstDay.temp_max), ''); } let cardsHtml = ''; for (const day of data.forecast) { const icon = getWeatherIcon(day.condition); cardsHtml += `
${day.day.substring(0, 3)}
${icon}
${Math.round(day.temp_max)}°
${day.rain_chance}%
`; } forecastCards.innerHTML = cardsHtml; if (weatherWidget) weatherWidget.style.display = 'block'; } catch (error) { console.error('Weather widget error:', error); if (forecastCards) forecastCards.innerHTML = '
Error loading forecast
'; } } // Render forecast sidebar from a weather_data object (returned by chat endpoint) function renderForecastFromData(weatherData) { if (!weatherData || !weatherData.forecast || !forecastCards) return; if (weatherData.forecast.length > 0) { const first = weatherData.forecast[0]; updateWeatherSummaryInAccordion(first.condition, Math.round(first.temp_max), ''); } let cardsHtml = ''; for (const day of weatherData.forecast) { const icon = getWeatherIcon(day.condition); cardsHtml += `
${day.day.substring(0, 3)}
${icon}
${Math.round(day.temp_max)}°
${day.rain_chance}%
`; } forecastCards.innerHTML = cardsHtml; if (weatherWidget) weatherWidget.style.display = 'block'; } function updateSensorSummaryInAccordion(moisture, temp) { const sensorHeaderSpan = document.querySelector('.accordion-header[data-accordion="sensorData"] .accordion-title'); if (sensorHeaderSpan) { sensorHeaderSpan.innerHTML = `📊 Sensor Data (Soil: ${moisture || '--'}%, ${temp || '--'}°C)`; } } function updateCropSummaryInAccordion(crop, bbch) { const cropHeaderSpan = document.querySelector('.accordion-header[data-accordion="cropInfo"] .accordion-title'); if (cropHeaderSpan) { const cropDisplay = crop || 'Not set'; const bbchDisplay = bbch || '--'; cropHeaderSpan.innerHTML = `🌾 Crop Info (${cropDisplay}, BBCH ${bbchDisplay})`; } } // Monitor crop type and BBCH changes — mark dirty so next message includes updated values if (cropType) { cropType.addEventListener('change', function() { cropDirty = true; bbchDirty = true; // reset BBCH too when crop changes updateCropSummaryInAccordion(cropType.value, bbchStage.value); }); } if (bbchStage) { bbchStage.addEventListener('input', function() { bbchDirty = true; updateCropSummaryInAccordion(cropType.value, bbchStage.value); }); } // Monitor sensor inputs if (soilMoisture) { soilMoisture.addEventListener('input', function() { updateSensorSummaryInAccordion(soilMoisture.value, soilTemp.value); }); } if (soilTemp) { soilTemp.addEventListener('input', function() { updateSensorSummaryInAccordion(soilMoisture.value, soilTemp.value); }); } // Get Forecast button handler (city mode) if (getForecastBtn) { getForecastBtn.addEventListener('click', function() { const location = locationInput.value.trim(); if (!location) { alert('Please enter a location first'); return; } updateForecastWidget(location); }); } function isWeatherQuestion(message) { const weatherKeywords = ['weather', 'forecast', 'rain', 'temperature', 'temp', 'sunny', 'cloudy', 'storm', 'humid']; const msgLower = message.toLowerCase(); return weatherKeywords.some(keyword => msgLower.includes(keyword)); } // Handle image attachment with thumbnail preview const ALLOWED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'image/gif']; const MAX_IMAGE_BYTES = 5 * 1024 * 1024; // 5 MB if (imageInput) { imageInput.addEventListener('change', function(e) { const file = e.target.files[0]; if (!file) return; if (!ALLOWED_IMAGE_TYPES.includes(file.type)) { alert('Only image files are supported (JPEG, PNG, WebP, GIF).'); imageInput.value = ''; return; } if (file.size > MAX_IMAGE_BYTES) { alert('Image is too large. Please use an image under 5 MB.'); imageInput.value = ''; return; } const reader = new FileReader(); reader.onloadend = function() { attachedImageBase64 = reader.result; if (thumbImg) thumbImg.src = attachedImageBase64; if (imagePreviewThumb) imagePreviewThumb.style.display = 'flex'; }; reader.readAsDataURL(file); }); } // Remove attached image thumbnail if (removeThumbBtn) { removeThumbBtn.addEventListener('click', function() { attachedImageBase64 = null; if (imageInput) imageInput.value = ''; if (imagePreviewThumb) imagePreviewThumb.style.display = 'none'; if (thumbImg) thumbImg.src = ''; }); } // Send chat message async function sendMessage() { const message = questionInput.value.trim(); if (!message && !attachedImageBase64) return; console.log("=== SEND MESSAGE DEBUG ==="); console.log("Message:", message); console.log("attachedImageBase64 exists:", attachedImageBase64 ? "YES" : "NO"); if (attachedImageBase64) { console.log("Image length:", attachedImageBase64.length); } // Store image before clearing const imageToSend = attachedImageBase64; // Show user message with image (if any) if (imageToSend) { addUserMessageWithImage(message, imageToSend); } else { addMessageStatic(message, true); } questionInput.value = ''; showLoading(); // Crop and BBCH are ALWAYS enabled - no toggle checks needed const sensorsEnabled = sensorsToggle ? sensorsToggle.checked : true; const weatherEnabled = weatherToggle ? weatherToggle.checked : true; const requestBody = { message: message, session_id: sessionId }; // Add image if we have one if (imageToSend) { requestBody.image_base64 = imageToSend; console.log("Added image to requestBody, length:", imageToSend.length); } // Send crop/BBCH only on first message or when user explicitly changed them if (cropDirty && cropType.value) { requestBody.crop_type = cropType.value; cropDirty = false; } // Always send farming_type if a value is selected — backend handles smart injection if (farmingType && farmingType.value) { requestBody.farming_type = farmingType.value; } if (bbchDirty && bbchStage.value) { requestBody.bbch_stage = bbchStage.value; bbchDirty = false; } if (locationInput.value) { requestBody.location = locationInput.value; } if (sensorsEnabled) { if (soilMoisture.value) requestBody.soil_moisture = parseFloat(soilMoisture.value); if (soilTemp.value) requestBody.soil_temperature = parseFloat(soilTemp.value); if (airTemp.value) requestBody.air_temperature = parseFloat(airTemp.value); if (humidity.value) requestBody.humidity = parseFloat(humidity.value); } requestBody.sensors_enabled = sensorsEnabled; requestBody.weather_enabled = weatherEnabled; requestBody.crop_detection_enabled = true; // ALWAYS enabled requestBody.bbch_detection_enabled = true; // ALWAYS enabled // Clear the attached image AFTER building request body attachedImageBase64 = null; if (imageInput) imageInput.value = ''; if (imagePreviewThumb) imagePreviewThumb.style.display = 'none'; if (thumbImg) thumbImg.src = ''; console.log("Request body keys:", Object.keys(requestBody)); console.log("Sending request..."); try { const response = await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody) }); console.log("Response status:", response.status); if (!response.ok) { const error = await response.json(); throw new Error(error.detail || 'Chat failed'); } const data = await response.json(); hideLoading(); await addMessageWithTyping(data.response, false); if (data.context_used && data.context_used.includes('crop_context')) { updateCropSummaryInAccordion(cropType.value, bbchStage.value); } // Use weather data returned by the chat endpoint (single source of truth) if (data.weather_data && data.weather_data.forecast) { renderForecastFromData(data.weather_data); } } catch (error) { hideLoading(); const userMsg = error.message && error.message !== 'Failed to fetch' ? error.message : 'Connection error. Please check your network and try again.'; addMessageStatic('Sorry, something went wrong: ' + userMsg, false); console.error(error); } } function clearChat() { // Clear old session memory on backend before generating new session fetch('/api/clear-session', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ session_id: sessionId }), }).catch(() => {}); // fire-and-forget, don't block UI chatBox.innerHTML = '
👋 Hello! I\'m AllTech AI. Ask me about crop management, fertilizers, irrigation, or diseases.
'; attachedImageBase64 = null; cropDirty = true; bbchDirty = true; if (imageInput) imageInput.value = ''; if (imagePreviewThumb) imagePreviewThumb.style.display = 'none'; if (thumbImg) thumbImg.src = ''; if (weatherWidget) weatherWidget.style.display = 'none'; sessionId = generateSessionId(); console.log('New session ID:', sessionId); } sendBtn.addEventListener('click', sendMessage); clearBtn.addEventListener('click', clearChat); questionInput.addEventListener('keydown', function(e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } }); const attachBtn = document.querySelector('.pill-attach'); if (attachBtn) { attachBtn.addEventListener('click', function() { if (imageInput) imageInput.click(); }); } updateCropSummaryInAccordion(cropType.value, bbchStage.value); updateSensorSummaryInAccordion(soilMoisture.value, soilTemp.value);