| document.addEventListener('DOMContentLoaded', () => { |
| const input = document.getElementById('question'); |
| const sendBtn = document.getElementById('sendBtn'); |
| const chatContainer = document.getElementById('chat'); |
| const globalIntent = document.getElementById('global-intent'); |
| const globalConfidence = document.getElementById('global-confidence'); |
| const inputContainer = document.getElementById('input-container'); |
|
|
| let isWaiting = false; |
| let messageCounter = 0; |
|
|
| |
| input.addEventListener('input', function() { |
| this.style.height = 'auto'; |
| this.style.height = (this.scrollHeight) + 'px'; |
| if (parseFloat(this.style.height) > 200) { |
| this.style.overflowY = 'auto'; |
| } else { |
| this.style.overflowY = 'hidden'; |
| } |
| |
| |
| if(this.value.trim().length > 0 && !isWaiting) { |
| sendBtn.removeAttribute('disabled'); |
| } else { |
| sendBtn.setAttribute('disabled', 'true'); |
| } |
| }); |
|
|
| input.addEventListener('focus', () => inputContainer.classList.add('focused')); |
| input.addEventListener('blur', () => inputContainer.classList.remove('focused')); |
|
|
| input.addEventListener('keydown', (e) => { |
| if (e.key === 'Enter' && !e.shiftKey) { |
| e.preventDefault(); |
| sendMessage(); |
| } |
| }); |
|
|
| sendBtn.addEventListener('click', () => { |
| sendMessage(); |
| }); |
|
|
| |
| chatContainer.addEventListener('click', (e) => { |
| if (e.target.classList.contains('rec-chip')) { |
| const question = e.target.textContent; |
| input.value = question; |
| |
| input.dispatchEvent(new Event('input')); |
| sendMessage(); |
| } |
| }); |
|
|
| async function sendMessage() { |
| const text = input.value.trim(); |
| if (!text || isWaiting) return; |
|
|
| |
| input.value = ''; |
| input.style.height = 'auto'; |
| sendBtn.setAttribute('disabled', 'true'); |
|
|
| |
| appendMessage('user', text); |
| |
| |
| scrollToBottom(); |
|
|
| |
| isWaiting = true; |
| const loadingId = appendLoading(); |
| scrollToBottom(); |
|
|
| try { |
| const response = await fetch('/ask', { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json', |
| }, |
| body: JSON.stringify({ question: text }) |
| }); |
|
|
| if (!response.ok) { |
| throw new Error('Erreur de communication avec le serveur'); |
| } |
|
|
| const data = await response.json(); |
| |
| |
| document.getElementById(loadingId).remove(); |
|
|
| |
| handleBotResponse(data); |
|
|
| } catch (error) { |
| console.error('Error:', error); |
| document.getElementById(loadingId).remove(); |
| appendMessage('bot', 'Désolé, une erreur est survenue lors de la communication avec le serveur.'); |
| } finally { |
| isWaiting = false; |
| if(input.value.trim().length > 0) { |
| sendBtn.removeAttribute('disabled'); |
| } |
| scrollToBottom(); |
| } |
| } |
|
|
| function appendMessage(role, text) { |
| messageCounter++; |
| const row = document.createElement('div'); |
| row.className = `message-row ${role}`; |
| |
| const avatarSvg = role === 'user' |
| ? '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>' |
| : '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"></path><path d="M7 10v4"></path><path d="M11 10v4"></path></svg>'; |
|
|
| |
| const formattedText = text.replace(/\n/g, '<br>'); |
|
|
| row.innerHTML = ` |
| <div class="message-content"> |
| <div class="avatar ${role}">${avatarSvg}</div> |
| <div class="message-body"> |
| <p>${formattedText}</p> |
| </div> |
| </div> |
| `; |
| |
| chatContainer.appendChild(row); |
| return row; |
| } |
|
|
| function appendLoading() { |
| const id = 'loading-' + Date.now(); |
| const row = document.createElement('div'); |
| row.className = 'message-row bot'; |
| row.id = id; |
| |
| row.innerHTML = ` |
| <div class="message-content"> |
| <div class="avatar bot"> |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"></path><path d="M7 10v4"></path><path d="M11 10v4"></path></svg> |
| </div> |
| <div class="message-body"> |
| <div class="typing-indicator"> |
| <div class="typing-dot"></div> |
| <div class="typing-dot"></div> |
| <div class="typing-dot"></div> |
| </div> |
| </div> |
| </div> |
| `; |
| |
| chatContainer.appendChild(row); |
| return id; |
| } |
|
|
| function handleBotResponse(data) { |
| messageCounter++; |
| const row = document.createElement('div'); |
| row.className = 'message-row bot'; |
| |
| const contentDiv = document.createElement('div'); |
| contentDiv.className = 'message-content'; |
|
|
| const avatar = document.createElement('div'); |
| avatar.className = 'avatar bot'; |
| avatar.innerHTML = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"></path><path d="M7 10v4"></path><path d="M11 10v4"></path></svg>'; |
| |
| const bodyDiv = document.createElement('div'); |
| bodyDiv.className = 'message-body'; |
|
|
| |
| const textP = document.createElement('p'); |
| textP.innerHTML = (data.response || '').replace(/\n/g, '<br>'); |
| bodyDiv.appendChild(textP); |
|
|
| |
| if (data.intent) globalIntent.textContent = `Intent: ${data.intent}`; |
| if (data.confidence !== undefined) { |
| const confVal = typeof data.confidence === 'number' ? data.confidence.toFixed(1) : parseFloat(data.confidence).toFixed(1); |
| globalConfidence.textContent = `Confiance: ${confVal}%`; |
| } |
|
|
| |
| const richContainer = document.createElement('div'); |
| richContainer.className = 'rich-content'; |
|
|
| |
| const hasService = data.service && data.service !== ''; |
| const hasLocation = data.lat !== null && data.lon !== null && data.lat !== undefined; |
| |
| if (hasService || hasLocation || (data.link && data.link !== '')) { |
| const serviceCard = document.createElement('div'); |
| serviceCard.className = 'service-card'; |
| |
| let btnHtml = ''; |
| if (data.link && data.link !== '') { |
| |
| btnHtml = ` |
| <a href="${data.link}" target="_blank" class="btn btn-outline" title="${data.link}"> |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg> |
| Ouvrir le lien |
| </a>`; |
| } |
|
|
| let navBtnHtml = ''; |
| if (data.lat && data.lon) { |
| const mapUrl = `https://www.google.com/maps?q=${data.lat},${data.lon}`; |
| navBtnHtml = ` |
| <a href="${mapUrl}" target="_blank" class="btn btn-nav"> |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="3 11 22 2 13 21 11 13 3 11"></polygon></svg> |
| Y aller |
| </a>`; |
| } |
|
|
| const serviceName = hasService ? data.service : (hasLocation ? "Emplacement trouvé" : "Lien identifié"); |
| |
| serviceCard.innerHTML = ` |
| <div class="service-info"> |
| <h3>${serviceName}</h3> |
| <p>Information identifiée pour cette requête</p> |
| </div> |
| <div class="service-actions"> |
| ${btnHtml} |
| ${navBtnHtml} |
| </div> |
| `; |
| richContainer.appendChild(serviceCard); |
| } |
|
|
| |
| let mapId = null; |
| if (data.lat && data.lon) { |
| mapId = `map-${messageCounter}`; |
| const mapWrapper = document.createElement('div'); |
| mapWrapper.className = 'map-container'; |
| mapWrapper.id = mapId; |
| richContainer.appendChild(mapWrapper); |
| } |
|
|
| |
| if (data.recs && Array.isArray(data.recs) && data.recs.length > 0) { |
| const recsWrapper = document.createElement('div'); |
| recsWrapper.className = 'recs-container'; |
| data.recs.forEach(rec => { |
| if (rec.trim() !== '') { |
| const chip = document.createElement('button'); |
| chip.className = 'rec-chip'; |
| chip.textContent = rec; |
| recsWrapper.appendChild(chip); |
| } |
| }); |
| richContainer.appendChild(recsWrapper); |
| } |
|
|
| if (richContainer.children.length > 0) { |
| bodyDiv.appendChild(richContainer); |
| } |
| |
| |
| const analyticsP = document.createElement('div'); |
| analyticsP.className = 'analytics-data'; |
| analyticsP.innerHTML = ` |
| <span> |
| <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polyline points="12 16 16 12 12 8"></polyline><line x1="8" y1="12" x2="16" y2="12"></line></svg> |
| Intent: ${data.intent || 'N/A'} |
| </span> |
| <span> |
| <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg> |
| Confiance: ${data.confidence !== undefined ? (typeof data.confidence === 'number' ? data.confidence.toFixed(1) : parseFloat(data.confidence).toFixed(1)) : '--'}% |
| </span> |
| `; |
| bodyDiv.appendChild(analyticsP); |
|
|
| contentDiv.appendChild(avatar); |
| contentDiv.appendChild(bodyDiv); |
| row.appendChild(contentDiv); |
| chatContainer.appendChild(row); |
|
|
|
|
|
|
| |
| if (mapId) { |
| setTimeout(() => { |
| initMap(mapId, parseFloat(data.lat), parseFloat(data.lon), data.service || 'Emplacement'); |
| }, 100); |
| } |
|
|
| scrollToBottom(); |
| } |
|
|
| function initMap(elementId, lat, lon, title) { |
| const map = L.map(elementId).setView([lat, lon], 15); |
| |
| |
| L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { |
| attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> © <a href="https://carto.com/">CARTO</a>', |
| subdomains: 'abcd', |
| maxZoom: 20 |
| }).addTo(map); |
|
|
| const marker = L.marker([lat, lon]).addTo(map); |
| marker.bindPopup(`<b>${title}</b>`).openPopup(); |
| |
| |
| setTimeout(() => map.invalidateSize(), 300); |
| } |
|
|
|
|
|
|
| function scrollToBottom() { |
| chatContainer.scrollTop = chatContainer.scrollHeight; |
| } |
|
|
| |
| |
| |
| const globalMapBtn = document.getElementById('global-map-btn'); |
| const globalModal = document.getElementById('global-map-modal'); |
| const closeModal = document.getElementById('close-modal-span'); |
| let globalLeafletMap = null; |
|
|
| if (globalMapBtn) { |
| globalMapBtn.addEventListener('click', async () => { |
| globalModal.style.display = 'block'; |
|
|
| |
| if (!globalLeafletMap) { |
| globalLeafletMap = L.map('global-map').setView([34.88, -1.30], 13); |
| L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { |
| attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>', |
| subdomains: 'abcd', |
| maxZoom: 20 |
| }).addTo(globalLeafletMap); |
| } |
|
|
| |
| setTimeout(() => globalLeafletMap.invalidateSize(), 300); |
|
|
| |
| try { |
| const resp = await fetch('/api/services'); |
| const data = await resp.json(); |
| |
| if (data.status === 'success' && data.services && data.services.length > 0) { |
| const bounds = []; |
| data.services.forEach(srv => { |
| const marker = L.marker([srv.lat, srv.lon]).addTo(globalLeafletMap); |
| |
| |
| const popupContent = ` |
| <div style="text-align:center;"> |
| <b style="display:block;margin-bottom:8px;">${srv.service}</b> |
| <button onclick="document.getElementById('question').value='${srv.service.replace(/'/g, "\\'")}'; document.getElementById('sendBtn').click(); document.getElementById('global-map-modal').style.display='none';" style="background:var(--accent-color);color:white;border:none;padding:5px 10px;border-radius:4px;cursor:pointer;">Demander</button> |
| </div> |
| `; |
| marker.bindPopup(popupContent); |
| bounds.push([srv.lat, srv.lon]); |
| }); |
| |
| // Center map on all pins |
| if (bounds.length > 0) { |
| globalLeafletMap.fitBounds(bounds, {padding: [50, 50]}); |
| } |
| } |
| } catch(e) { |
| console.error("Error loading global map services", e); |
| } |
| }); |
| } |
| |
| if (closeModal) { |
| closeModal.addEventListener('click', () => { |
| globalModal.style.display = 'none'; |
| }); |
| } |
| |
| window.addEventListener('click', (e) => { |
| if (e.target == globalModal) { |
| globalModal.style.display = 'none'; |
| } |
| }); |
| |
| }); |
| |