Spaces:
Sleeping
Sleeping
| // ============================================ | |
| // 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 = '<div class="forecast-placeholder">Loading forecast...</div>'; | |
| 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 += ` | |
| <div class="forecast-card"> | |
| <div class="day">${day.day.substring(0, 3)}</div> | |
| <div class="icon">${icon}</div> | |
| <div class="temp">${Math.round(day.temp_max)}°</div> | |
| <div class="rain">${day.rain_chance}%</div> | |
| </div> | |
| `; | |
| } | |
| forecastCards.innerHTML = cardsHtml; | |
| if (weatherWidget) weatherWidget.style.display = 'block'; | |
| } catch (error) { | |
| console.error('Weather widget error:', error); | |
| forecastCards.innerHTML = '<div class="forecast-placeholder">Error loading forecast</div>'; | |
| } | |
| } | |
| 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 = '<option value="">-- Select phenology stage --</option>'; | |
| 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 = '<option value="">-- Select phenology stage --</option>'; | |
| for (const stage of stages) { | |
| options += `<option value="${stage.bbch_range}" data-bbch="${stage.bbch_range}">${stage.phenology_name} (BBCH ${stage.bbch_range})</option>`; | |
| } | |
| phenologyStage.innerHTML = options; | |
| } catch (error) { | |
| console.error('Error loading phenology:', error); | |
| phenologyStage.innerHTML = '<option value="">-- No phenology data --</option>'; | |
| } | |
| } | |
| // 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 += '<strong>'; | |
| inBold = true; | |
| } else { | |
| currentHtml += '</strong>'; | |
| inBold = false; | |
| } | |
| k++; | |
| } else if (char === '*') { | |
| if (!inItalic) { | |
| currentHtml += '<em>'; | |
| inItalic = true; | |
| } else { | |
| currentHtml += '</em>'; | |
| inItalic = false; | |
| } | |
| } else if (char === '\n') { | |
| currentHtml += '<br>'; | |
| } else { | |
| currentHtml += char; | |
| } | |
| } | |
| messageDiv.innerHTML = currentHtml + '<span class="typing-cursor">|</span>'; | |
| i++; | |
| setTimeout(typeNextChar, speed); | |
| } else { | |
| let finalHtml = messageDiv.innerHTML; | |
| finalHtml = finalHtml.replace('<span class="typing-cursor">|</span>', ''); | |
| 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, '<strong>$1</strong>') | |
| .replace(/\*(.*?)\*/g, '<em>$1</em>') | |
| .replace(/\n/g, '<br>'); | |
| 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 = '<div class="forecast-placeholder">Enter a location and click "Get Forecast"</div>'; | |
| 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 += ` | |
| <div class="forecast-card"> | |
| <div class="day">${day.day.substring(0, 3)}</div> | |
| <div class="icon">${icon}</div> | |
| <div class="temp">${Math.round(day.temp_max)}°</div> | |
| <div class="rain">${day.rain_chance}%</div> | |
| </div> | |
| `; | |
| } | |
| forecastCards.innerHTML = cardsHtml; | |
| if (weatherWidget) weatherWidget.style.display = 'block'; | |
| } catch (error) { | |
| console.error('Weather widget error:', error); | |
| if (forecastCards) forecastCards.innerHTML = '<div class="forecast-placeholder">Error loading forecast</div>'; | |
| } | |
| } | |
| // 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 += ` | |
| <div class="forecast-card"> | |
| <div class="day">${day.day.substring(0, 3)}</div> | |
| <div class="icon">${icon}</div> | |
| <div class="temp">${Math.round(day.temp_max)}°</div> | |
| <div class="rain">${day.rain_chance}%</div> | |
| </div> | |
| `; | |
| } | |
| 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 = '<div class="bot-message">👋 Hello! I\'m AllTech AI. Ask me about crop management, fertilizers, irrigation, or diseases.</div>'; | |
| 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); |