let conversation = [ { role: 'bot', message: 'Hello! I’m Chef Bot, your culinary assistant! What’s your name?' } ]; let selectedItems = []; let selectionBoxVisible = false; const searchCache = new Map(); const MAX_RETRIES = 3; function addMessage(role, message, isDebug = false) { const chatMessages = document.getElementById('chatMessages'); if (!chatMessages) { console.error('Chat messages container not found!'); return; } const messageDiv = document.createElement('div'); messageDiv.className = role === 'bot' ? 'bot-message' : 'user-message'; messageDiv.textContent = message; chatMessages.appendChild(messageDiv); chatMessages.scrollTop = chatMessages.scrollHeight; if (isDebug) console.log(`[${role.toUpperCase()}] ${message}`); } function showToast(message) { const toast = document.createElement('div'); toast.className = 'toast'; toast.textContent = message; document.body.appendChild(toast); setTimeout(() => toast.remove(), 3000); } function sendMessage() { const userInput = document.getElementById('userInput'); if (!userInput) { console.error('User input field not found!'); return; } const message = userInput.value.trim(); if (message) { addMessage('user', message, true); conversation.push({ role: 'user', message: message }); selectionBoxVisible = true; handleResponse(message); showToast(`Sent: "${message}"`); } else { addMessage('bot', 'Please type a dish or preference! 😄'); showToast('Input is empty!'); } userInput.value = ''; updateSelectionBox(); } function handleResponse(userInput) { const lowerInput = userInput.toLowerCase(); let botResponse = ''; if (conversation.length === 2) { botResponse = `Hi ${userInput}! 🍳 Search for a dish or choose a preference below!`; displayOptions([ { text: 'Vegetarian', class: 'green' }, { text: 'Non-Vegetarian', class: 'red' }, { text: 'Both', class: 'gray' } ]); addMessage('bot', botResponse); } else if (lowerInput === 'vegetarian' || lowerInput === 'non-vegetarian' || lowerInput === 'both') { botResponse = `Fetching ${lowerInput} dishes...`; addMessage('bot', botResponse); fetchMenuItems(lowerInput); } else { botResponse = `Looking for "${userInput}"...`; addMessage('bot', botResponse); fetchMenuItems(null, userInput); } } function debounce(func, wait) { let timeout; return function (...args) { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), wait); }; } function updateSelectionBox() { const chatMessages = document.getElementById('chatMessages'); if (!chatMessages) return; const existingBox = document.querySelector('.selection-box'); if (existingBox) existingBox.remove(); if (!selectionBoxVisible && selectedItems.length === 0) return; const selectionBox = document.createElement('div'); selectionBox.className = 'selection-box'; const clearAllButton = document.createElement('button'); clearAllButton.textContent = 'Clear All'; clearAllButton.className = 'remove-button'; clearAllButton.setAttribute('aria-label', 'Clear all selected items'); clearAllButton.tabIndex = 0; clearAllButton.onclick = () => { selectedItems = []; addMessage('bot', 'Cleared all items.'); showToast('All items cleared!'); updateSelectionBox(); }; selectionBox.appendChild(clearAllButton); const vegButton = document.createElement('button'); vegButton.textContent = 'Veg'; vegButton.className = 'option-button green'; vegButton.setAttribute('aria-label', 'Select vegetarian dishes'); vegButton.tabIndex = 0; vegButton.onclick = () => { addMessage('user', 'Vegetarian', true); conversation.push({ role: 'user', message: 'Vegetarian' }); handleResponse('vegetarian'); showToast('Selected Vegetarian'); }; selectionBox.appendChild(vegButton); const nonVegButton = document.createElement('button'); nonVegButton.textContent = 'Non-Veg'; nonVegButton.className = 'option-button red'; nonVegButton.setAttribute('aria-label', 'Select non-vegetarian dishes'); nonVegButton.tabIndex = 0; nonVegButton.onclick = () => { addMessage('user', 'Non-Vegetarian', true); conversation.push({ role: 'user', message: 'Non-Vegetarian' }); handleResponse('non-vegetarian'); showToast('Selected Non-Vegetarian'); }; selectionBox.appendChild(nonVegButton); const bothButton = document.createElement('button'); bothButton.textContent = 'Both'; bothButton.className = 'option-button gray'; bothButton.setAttribute('aria-label', 'Select both vegetarian and non-vegetarian dishes'); bothButton.tabIndex = 0; bothButton.onclick = () => { addMessage('user', 'Both', true); conversation.push({ role: 'user', message: 'Both' }); handleResponse('both'); showToast('Selected Both'); }; selectionBox.appendChild(bothButton); const label = document.createElement('span'); label.textContent = 'Selected:'; selectionBox.appendChild(label); selectedItems.forEach((item, index) => { const itemContainer = document.createElement('div'); itemContainer.className = 'selected-item'; itemContainer.dataset.hidden = item.source === 'Sector_Detail__c' ? 'true' : 'false'; const img = document.createElement('img'); img.src = item.image_url || 'https://via.placeholder.com/30?text=Item'; img.alt = item.name; img.className = 'selected-item-image'; itemContainer.appendChild(img); const contentDiv = document.createElement('div'); contentDiv.className = 'selected-item-content'; const itemSpan = document.createElement('span'); itemSpan.textContent = `${item.name} (Qty: ${item.quantity || 1})`; contentDiv.appendChild(itemSpan); const quantityDiv = document.createElement('div'); quantityDiv.className = 'quantity-controls'; const decButton = document.createElement('button'); decButton.textContent = '-'; decButton.className = 'quantity-button'; decButton.setAttribute('aria-label', `Decrease quantity of ${item.name}`); decButton.tabIndex = 0; decButton.onclick = () => { if (item.quantity > 1) { item.quantity -= 1; updateSelectionBox(); showToast(`Decreased quantity of ${item.name}`); } }; quantityDiv.appendChild(decButton); const incButton = document.createElement('button'); incButton.textContent = '+'; incButton.className = 'quantity-button'; incButton.setAttribute('aria-label', `Increase quantity of ${item.name}`); incButton.tabIndex = 0; incButton.onclick = () => { item.quantity += 1; updateSelectionBox(); showToast(`Increased quantity of ${item.name}`); }; quantityDiv.appendChild(incButton); contentDiv.appendChild(quantityDiv); if (item.source === 'Sector_Detail__c') { const showButton = document.createElement('button'); showButton.textContent = 'Show'; showButton.className = 'show-button'; showButton.setAttribute('aria-label', `Show details for ${item.name}`); showButton.tabIndex = 0; showButton.onclick = () => toggleDescription(itemContainer, item.description, item.name); contentDiv.appendChild(showButton); } itemContainer.appendChild(contentDiv); const removeButton = document.createElement('button'); removeButton.textContent = 'X'; removeButton.className = 'remove-button'; removeButton.setAttribute('aria-label', `Remove ${item.name} from selection`); removeButton.tabIndex = 0; removeButton.onclick = () => { selectedItems.splice(index, 1); addMessage('bot', `Removed "${item.name}".`); showToast(`Removed "${item.name}"`); updateSelectionBox(); }; itemContainer.appendChild(removeButton); selectionBox.appendChild(itemContainer); }); const textInput = document.createElement('input'); textInput.type = 'text'; textInput.placeholder = 'Add item...'; textInput.className = 'manual-input'; textInput.setAttribute('aria-label', 'Add item manually'); textInput.tabIndex = 0; const debouncedFetch = debounce((e) => { if (e.key === 'Enter' && textInput.value.trim()) { const itemName = textInput.value.trim(); fetchSectorItemDetails(itemName); textInput.value = ''; showToast(`Searching for "${itemName}"...`); } }, 300); textInput.addEventListener('keypress', debouncedFetch); selectionBox.appendChild(textInput); if (selectedItems.length > 0) { const quantityInput = document.createElement('input'); quantityInput.type = 'number'; quantityInput.min = '1'; quantityInput.value = '1'; quantityInput.placeholder = 'Qty'; quantityInput.className = 'quantity-input'; quantityInput.setAttribute('aria-label', 'Set default quantity for all items'); quantityInput.tabIndex = 0; selectionBox.appendChild(quantityInput); const submitButton = document.createElement('button'); submitButton.textContent = 'Submit'; submitButton.className = 'submit-button'; submitButton.setAttribute('aria-label', 'Submit selected items'); submitButton.tabIndex = 0; submitButton.onclick = () => promptAndSubmit(quantityInput.value); selectionBox.appendChild(submitButton); const orderNameInput = document.createElement('input'); orderNameInput.type = 'text'; orderNameInput.placeholder = 'Order Name'; orderNameInput.className = 'order-name-input'; orderNameInput.setAttribute('aria-label', 'Enter order name'); orderNameInput.tabIndex = 0; selectionBox.appendChild(orderNameInput); } chatMessages.appendChild(selectionBox); chatMessages.scrollTop = chatMessages.scrollHeight; } async function fetchWithRetry(url, options, retries = MAX_RETRIES) { for (let i = 0; i < retries; i++) { try { const response = await fetch(url, options); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); return await response.json(); } catch (error) { if (i === retries - 1) { throw error; } await new Promise(resolve => setTimeout(resolve, 2000)); } } } function fetchMenuItems(dietaryPreference = '', searchTerm = '') { const cacheKey = `${dietaryPreference}:${searchTerm}`; if (searchCache.has(cacheKey)) { const data = searchCache.get(cacheKey); displayCachedMenuItems(data, searchTerm, dietaryPreference); return; } const payload = {}; if (dietaryPreference) payload.dietary_preference = dietaryPreference; if (searchTerm) payload.search_term = searchTerm; const chatMessages = document.getElementById('chatMessages'); const spinner = document.createElement('div'); spinner.className = 'spinner'; chatMessages.appendChild(spinner); fetchWithRetry('/get_menu_items', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }) .then(data => { spinner.remove(); if (data.error) { addMessage('bot', `Error: ${data.error}. Try again!`); showToast(`Error: ${data.error}`); } else if (data.menu_items.length > 0) { searchCache.set(cacheKey, data); addMessage('bot', `--- Found ${data.menu_items.length} item${data.menu_items.length > 1 ? 's' : ''} ---`); displayItemsList(data.menu_items); } else { addMessage('bot', `No matches for "${searchTerm || dietaryPreference}". Try "paneer" or "chicken curry"!`); showToast('No items found!'); } }) .catch(error => { spinner.remove(); addMessage('bot', `Connection issue: ${error.message}. Please try again later.`); showToast('Connection issue!'); }); } function displayCachedMenuItems(data, searchTerm, dietaryPreference) { if (data.error) { addMessage('bot', `Error: ${data.error}. Try again!`); showToast(`Error: ${data.error}`); } else if (data.menu_items.length > 0) { addMessage('bot', `--- Found ${data.menu_items.length} item${data.menu_items.length > 1 ? 's' : ''} ---`); displayItemsList(data.menu_items); } else { addMessage('bot', `No matches for "${searchTerm || dietaryPreference}". Try "paneer" or "chicken curry"!`); showToast('No items found!'); } } function fetchSectorItemDetails(itemName) { const cacheKey = `sector:${itemName}`; if (searchCache.has(cacheKey)) { const data = searchCache.get(cacheKey); displayCachedSectorItems(data, itemName); return; } fetchWithRetry('/get_sector_item_details', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ item_name: itemName }) }) .then(data => { if (data.error) { addMessage('bot', `No "${itemName}" found. Try another!`); showToast(`No "${itemName}" found`); } else { searchCache.set(cacheKey, data); const details = data.item_details; if (selectedItems.some(item => item.name === details.name)) { addMessage('bot', `"${details.name}" already selected!`); showToast(`"${details.name}" already selected`); } else { selectedItems.push({ ...details, quantity: 1 }); addMessage('bot', `Added "${details.name}"!`); showToast(`Added "${details.name}"`); updateSelectionBox(); } } }) .catch(error => { addMessage('bot', `Error for "${itemName}". Please try again later.`); showToast(`Error fetching "${itemName}"`); }); } function displayCachedSectorItems(data, itemName) { if (data.error) { addMessage('bot', `No "${itemName}" found. Try another!`); showToast(`No "${itemName}" found`); } else { const details = data.item_details; if (selectedItems.some(item => item.name === details.name)) { addMessage('bot', `"${details.name}" already selected!`); showToast(`"${details.name}" already selected`); } else { selectedItems.push({ ...details, quantity: 1 }); addMessage('bot', `Added "${details.name}"!`); showToast(`Added "${details.name}"`); updateSelectionBox(); } } } function toggleDescription(itemContainer, description, itemName) { let descElement = itemContainer.querySelector('.item-description'); if (!descElement) { descElement = document.createElement('p'); descElement.className = 'item-description'; descElement.textContent = description; itemContainer.querySelector('.selected-item-content').appendChild(descElement); itemContainer.dataset.hidden = 'false'; showToast(`Showing details for ${itemName}`); } else { descElement.remove(); itemContainer.dataset.hidden = 'true'; showToast(`Hid details for ${itemName}`); } } function displayItemsList(items) { const chatMessages = document.getElementById('chatMessages'); if (!chatMessages) { addMessage('bot', 'Display issue. Try again?'); showToast('Display issue!'); return; } const itemsGrid = document.createElement('div'); itemsGrid.className = 'items-grid'; items.forEach((item, index) => { const itemDiv = document.createElement('article'); itemDiv.className = 'item-card'; const img = document.createElement('img'); img.src = item.image_url || 'https://via.placeholder.com/120?text=Item'; img.alt = item.name; img.className = 'item-image'; if (index < 3) img.loading = 'eager'; // Preload first 3 images itemDiv.appendChild(img); const contentDiv = document.createElement('div'); contentDiv.className = 'item-content'; const nameDiv = document.createElement('div'); nameDiv.textContent = item.name; nameDiv.className = 'item-name'; contentDiv.appendChild(nameDiv); const fields = [ { label: 'Price', value: item.price ? `$${item.price.toFixed(2)}` : 'N/A' }, { label: 'Veg/Non-Veg', value: item.veg_nonveg }, { label: 'Spice', value: item.spice_levels }, { label: 'Category', value: item.category }, { label: 'Ingredients', value: item.ingredients }, { label: 'Nutrition', value: item.nutritional_info }, { label: 'Sector', value: item.sector }, { label: 'Dynamic', value: item.dynamic_dish ? 'Yes' : 'No' } ]; fields.forEach(field => { if (field.value) { const p = document.createElement('p'); p.className = 'item-field'; p.innerHTML = `${field.label}: ${field.value}`; contentDiv.appendChild(p); } }); itemDiv.appendChild(contentDiv); const buttonContainer = document.createElement('div'); buttonContainer.className = 'button-container'; const addButton = document.createElement('button'); addButton.textContent = 'Add'; addButton.className = 'add-button'; addButton.setAttribute('aria-label', `Add ${item.name} to selection`); addButton.tabIndex = 0; addButton.onclick = () => { const selectedItem = { name: item.name, image_url: item.image_url || '', category: item.category || 'Not specified', description: item.description || 'No description available', source: item.source, quantity: 1, ingredients: item.ingredients, nutritional_info: item.nutritional_info, price: item.price, sector: item.sector, spice_levels: item.spice_levels, veg_nonveg: item.veg_nonveg, dynamic_dish: item.dynamic_dish }; if (selectedItems.some(existing => existing.name === selectedItem.name)) { addMessage('bot', `"${selectedItem.name}" already selected!`); showToast(`"${selectedItem.name}" already selected`); } else { selectedItems.push(selectedItem); addMessage('bot', `Added "${selectedItem.name}"!`); showToast(`Added "${selectedItem.name}"`); updateSelectionBox(); } }; buttonContainer.appendChild(addButton); itemDiv.appendChild(buttonContainer); itemsGrid.appendChild(itemDiv); }); chatMessages.appendChild(itemsGrid); chatMessages.scrollTop = chatMessages.scrollHeight; } function displayOptions(options) { const chatMessages = document.getElementById('chatMessages'); if (!chatMessages) { console.error('Chat messages container not found!'); return; } const optionsDiv = document.createElement('div'); optionsDiv.className = 'options-container'; options.forEach(opt => { const button = document.createElement('button'); button.textContent = opt.text; button.className = `option-button ${opt.class}`; button.setAttribute('aria-label', `Select ${opt.text} dishes`); button.tabIndex = 0; button.onclick = () => { addMessage('user', opt.text, true); conversation.push({ role: 'user', message: opt.text }); selectionBoxVisible = true; handleResponse(opt.text); updateSelectionBox(); showToast(`Selected ${opt.text}`); }; optionsDiv.appendChild(button); }); const backButton = document.createElement('button'); backButton.textContent = 'Back'; backButton.className = 'option-button'; backButton.setAttribute('aria-label', 'Reset conversation'); backButton.tabIndex = 0; backButton.onclick = () => resetConversation(); optionsDiv.appendChild(backButton); chatMessages.appendChild(optionsDiv); } function promptAndSubmit(quantity) { const orderNameInput = document.querySelector('.order-name-input'); const customOrderName = orderNameInput ? orderNameInput.value.trim() : ''; const qty = parseInt(quantity); if (isNaN(qty) || qty <= 0) { addMessage('bot', 'Please enter a valid quantity (1 or more)!'); showToast('Invalid quantity!'); return; } if (confirm(`Submit ${selectedItems.length} items with default quantity ${qty}?`)) { submitToSalesforce(customOrderName, qty); showToast('Submitting order...'); } else { addMessage('bot', 'Cancelled. Add more items?'); showToast('Order cancelled'); } } function submitToSalesforce(customOrderName, quantity) { if (selectedItems.length === 0) { addMessage('bot', 'No items selected! Add some dishes! 😊'); showToast('No items selected'); return; } const itemsToSubmit = selectedItems.map(item => ({ name: item.name, category: item.category || 'Not specified', description: item.description || 'No description available', image_url: item.image_url || '', quantity: item.quantity || parseInt(quantity) || 1 })); fetchWithRetry('/submit_items', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ items: itemsToSubmit, custom_order_name: customOrderName }) }) .then(data => { if (data.error) { addMessage('bot', `Submission failed: ${data.error}. Try again?`); showToast(`Submission failed: ${data.error}`); } else { addMessage('bot', `Order "${customOrderName || 'Order'}" with ${itemsToSubmit.length} items submitted! What's next?`); selectedItems = []; updateSelectionBox(); showToast('Order submitted successfully!'); } }) .catch(error => { addMessage('bot', `Submission error: ${error.message}. Please try again later.`); showToast('Submission error!'); }); } function resetConversation() { const userName = conversation.length > 1 ? conversation[1].message : 'Friend'; conversation = [ { role: 'bot', message: `Hello! I’m Chef Bot, your culinary assistant! What’s your name?` }, { role: 'user', message: userName }, { role: 'bot', message: `Hi ${userName}! 🍳 Search for a dish or choose a preference below!` } ]; selectedItems = []; selectionBoxVisible = true; const chatMessages = document.getElementById('chatMessages'); chatMessages.innerHTML = ''; conversation.forEach(msg => addMessage(msg.role, msg.message)); displayOptions([ { text: 'Vegetarian', class: 'green' }, { text: 'Non-Vegetarian', class: 'red' }, { text: 'Both', class: 'gray' } ]); updateSelectionBox(); showToast('Conversation reset'); } document.getElementById('userInput').addEventListener('keypress', (e) => { if (e.key === 'Enter') sendMessage(); }); console.log('Chef Bot script loaded!');