Agridatalog / static /app.js
Agridatalogia's picture
Fix logo display: remove circle crop, fit horizontal wordmark; password → agri2026
8145303
Raw
History Blame Contribute Delete
28.9 kB
// ============================================
// 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);