| const state = { | |
| step: 'tileType', | |
| tileType: null, | |
| area: null, | |
| tileLength: null, | |
| tileWidth: null | |
| }; | |
| const chatArea = document.getElementById('chat-area'); | |
| const userInput = document.getElementById('user-input'); | |
| const recommendations = document.getElementById('recommendations'); | |
| const resetBtn = document.getElementById('reset-btn'); | |
| resetBtn.addEventListener('click', resetConversation); | |
| userInput.addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter') sendMessage(); | |
| }); | |
| resetConversation(); | |
| function resetConversation() { | |
| Object.assign(state, { | |
| step: 'tileType', | |
| tileType: null, | |
| area: null, | |
| tileLength: null, | |
| tileWidth: null | |
| }); | |
| chatArea.innerHTML = ` | |
| <div class="bot-message bg-gray-100 rounded-lg p-4 max-w-xs mb-3"> | |
| <p>Hello! ๐ I'm your Tile Calculator Assistant. Let's estimate how many tiles you need.</p> | |
| <p class="mt-2">Are you looking for <span class="font-semibold">floor</span> or <span class="font-semibold">wall</span> tiles?</p> | |
| <div class="flex gap-2 mt-3"> | |
| <button onclick="selectTileType('floor')" class="quick-reply bg-white text-indigo-600 border border-indigo-600 px-4 py-2 rounded-full font-medium hover:bg-indigo-50">Floor</button> | |
| <button onclick="selectTileType('wall')" class="quick-reply bg-white text-indigo-600 border border-indigo-600 px-4 py-2 rounded-full font-medium hover:bg-indigo-50">Wall</button> | |
| </div> | |
| </div> | |
| `; | |
| recommendations.innerHTML = ''; | |
| } | |
| function selectTileType(type) { | |
| state.tileType = type; | |
| state.step = 'area'; | |
| addMessage('user', `${type.charAt(0).toUpperCase() + type.slice(1)} tiles`); | |
| showTyping(); | |
| setTimeout(() => { | |
| hideTyping(); | |
| addMessage('bot', 'Great! What is the total area to cover (in sq.ft)?'); | |
| }, 800); | |
| } | |
| function sendMessage() { | |
| const message = userInput.value.trim(); | |
| if (!message) return; | |
| addMessage('user', message); | |
| userInput.value = ''; | |
| processUserMessage(message); | |
| } | |
| function processUserMessage(message) { | |
| showTyping(); | |
| setTimeout(() => { | |
| hideTyping(); | |
| if (state.step === 'area') { | |
| const area = parseFloat(message); | |
| if (isNaN(area) || area <= 0) { | |
| return addMessage('bot', 'โ Please enter a valid positive number for area (e.g., 100).'); | |
| } | |
| state.area = area; | |
| state.step = 'tileLength'; | |
| addMessage('bot', 'Now enter the tile length (in feet):'); | |
| } | |
| else if (state.step === 'tileLength') { | |
| const length = parseFloat(message); | |
| if (isNaN(length) || length <= 0) { | |
| return addMessage('bot', 'โ Please enter a valid positive number for tile length.'); | |
| } | |
| state.tileLength = length; | |
| state.step = 'tileWidth'; | |
| addMessage('bot', 'Now enter the tile width (in feet):'); | |
| } | |
| else if (state.step === 'tileWidth') { | |
| const width = parseFloat(message); | |
| if (isNaN(width) || width <= 0) { | |
| return addMessage('bot', 'โ Please enter a valid positive number for tile width.'); | |
| } | |
| state.tileWidth = width; | |
| calculateTiles(); | |
| } | |
| }, 800); | |
| } | |
| function calculateTiles() { | |
| const { tileLength, tileWidth, area, tileType } = state; | |
| const tileArea = tileLength * tileWidth; | |
| if (tileArea <= 0) { | |
| return addMessage('bot', `โ Invalid tile dimensions. Length ร width must be greater than 0.`); | |
| } | |
| if (tileArea > area) { | |
| return addMessage('bot', `โ The tile size (${tileLength}ร${tileWidth} = ${tileArea} sq.ft) is larger than the total area (${area} sq.ft). Please enter smaller tile dimensions.`); | |
| } | |
| if (tileArea > 20) { | |
| addMessage('bot', `๐ That's a large tile (${tileArea.toFixed(2)} sq.ft). Please confirm the size is correct.`); | |
| } | |
| const aspectRatio = Math.max(tileLength / tileWidth, tileWidth / tileLength); | |
| if (aspectRatio > 10) { | |
| addMessage('bot', `๐ This tile has a very high aspect ratio (${aspectRatio.toFixed(1)}:1). Please double-check if that's correct.`); | |
| } | |
| const numTiles = Math.ceil((area / tileArea) * 1.1); | |
| const numBoxes = Math.ceil(numTiles / 10); | |
| chatArea.insertAdjacentHTML('beforeend', ` | |
| <div class="bot-message bg-gray-100 rounded-lg p-4 mb-3"> | |
| <p class="font-semibold">Calculation Results:</p> | |
| <p>๐งฑ Tile Type: ${tileType}</p> | |
| <p>๐ Area to Cover: ${area} sq.ft</p> | |
| <p>๐งฎ Tile Size: ${tileLength} ft ร ${tileWidth} ft</p> | |
| <p class="mt-2">๐ข <strong>Tiles Needed:</strong> ${numTiles} (${numBoxes} boxes)</p> | |
| </div> | |
| `); | |
| state.step = 'complete'; | |
| const sizeLabel = `${Math.round(tileLength * 304.8)}x${Math.round(tileWidth * 304.8)} MM`; | |
| fetch('/recommend', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| tile_type: tileType, | |
| coverage: tileArea, | |
| area: area, | |
| preferred_sizes: [sizeLabel] | |
| }) | |
| }) | |
| .then(res => res.json()) | |
| .then(data => { | |
| if (data.recommended_products?.length) { | |
| loadProductRecommendations(data.recommended_products); | |
| } else { | |
| recommendations.innerHTML = `<div class="col-span-4 text-center py-4 text-gray-500">No product recommendations found.</div>`; | |
| } | |
| }) | |
| .catch(err => { | |
| console.error('Recommendation error:', err); | |
| recommendations.innerHTML = `<div class="col-span-4 text-center py-4 text-red-500">Error fetching recommendations.</div>`; | |
| }); | |
| } | |
| function loadProductRecommendations(products) { | |
| recommendations.innerHTML = ''; | |
| products.slice(0, 4).forEach(p => { | |
| recommendations.insertAdjacentHTML('beforeend', ` | |
| <div class="bg-white rounded-lg overflow-hidden shadow-sm border border-gray-100"> | |
| <div class="h-32 bg-gray-200 flex items-center justify-center"> | |
| ${p.image_url ? `<img src="${p.image_url}" class="h-full object-cover" alt="${p.name}">` : ` | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" /> | |
| </svg>`} | |
| </div> | |
| <div class="p-3"> | |
| <h4 class="font-medium text-gray-800">${p.name || 'Tile Product'}</h4> | |
| <p class="text-sm text-gray-600 mt-1">${p.price ? `โน${p.price}/box` : 'Price N/A'}</p> | |
| <a href="${p.url}" target="_blank" class="mt-2 inline-block text-sm text-indigo-600 underline">View Details</a> | |
| </div> | |
| </div> | |
| `); | |
| }); | |
| } | |
| function addMessage(sender, text) { | |
| const div = document.createElement('div'); | |
| div.className = `${sender}-message ${sender === 'user' ? 'ml-auto bg-indigo-600 text-white' : 'bg-gray-100'} rounded-lg p-4 max-w-xs mb-3`; | |
| div.textContent = text; | |
| chatArea.appendChild(div); | |
| chatArea.scrollTop = chatArea.scrollHeight; | |
| } | |
| function showTyping() { | |
| const typing = document.createElement('div'); | |
| typing.className = 'typing-indicator bg-gray-100 rounded-lg p-4 max-w-xs mb-3 flex gap-1'; | |
| typing.id = 'typing-indicator'; | |
| typing.innerHTML = '<span class="w-2 h-2 bg-gray-400 rounded-full"></span><span class="w-2 h-2 bg-gray-400 rounded-full"></span><span class="w-2 h-2 bg-gray-400 rounded-full"></span>'; | |
| chatArea.appendChild(typing); | |
| chatArea.scrollTop = chatArea.scrollHeight; | |
| } | |
| function hideTyping() { | |
| const el = document.getElementById('typing-indicator'); | |
| if (el) el.remove(); | |
| } | |