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; // To generate unique IDs for maps
// Auto-resize textarea
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';
}
// Enable/disable send button
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();
});
// Handle clicks on recommendation chips
chatContainer.addEventListener('click', (e) => {
if (e.target.classList.contains('rec-chip')) {
const question = e.target.textContent;
input.value = question;
// Hack to trigger auto-resize logic before sending
input.dispatchEvent(new Event('input'));
sendMessage();
}
});
async function sendMessage() {
const text = input.value.trim();
if (!text || isWaiting) return;
// Reset input
input.value = '';
input.style.height = 'auto';
sendBtn.setAttribute('disabled', 'true');
// Add User Message
appendMessage('user', text);
// Scroll to bottom
scrollToBottom();
// Show loading indicator
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();
// Remove loading
document.getElementById(loadingId).remove();
// Handle Bot Response
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'
? ''
: '';
// Format basic markdown-like text elements (newlines)
const formattedText = text.replace(/\n/g, '
');
row.innerHTML = `
`;
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 = `
`;
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 = '';
const bodyDiv = document.createElement('div');
bodyDiv.className = 'message-body';
// 1. Add Answer Text
const textP = document.createElement('p');
textP.innerHTML = (data.response || '').replace(/\n/g, '
');
bodyDiv.appendChild(textP);
// Update header tracking
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}%`;
}
// Rich Content Container
const richContainer = document.createElement('div');
richContainer.className = 'rich-content';
// 2. Service Card
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 !== '') {
// Ensure proper link parsing if needed
btnHtml = `
Ouvrir le lien
`;
}
let navBtnHtml = '';
if (data.lat && data.lon) {
const mapUrl = `https://www.google.com/maps?q=${data.lat},${data.lon}`;
navBtnHtml = `
Y aller
`;
}
const serviceName = hasService ? data.service : (hasLocation ? "Emplacement trouvé" : "Lien identifié");
serviceCard.innerHTML = `
${serviceName}
Information identifiée pour cette requête
${btnHtml}
${navBtnHtml}
`;
richContainer.appendChild(serviceCard);
}
// 3. Map Container
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);
}
// 4. Recommendations
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);
}
// Add analytics quietly at bottom of message
const analyticsP = document.createElement('div');
analyticsP.className = 'analytics-data';
analyticsP.innerHTML = `
Intent: ${data.intent || 'N/A'}
Confiance: ${data.confidence !== undefined ? (typeof data.confidence === 'number' ? data.confidence.toFixed(1) : parseFloat(data.confidence).toFixed(1)) : '--'}%
`;
bodyDiv.appendChild(analyticsP);
contentDiv.appendChild(avatar);
contentDiv.appendChild(bodyDiv);
row.appendChild(contentDiv);
chatContainer.appendChild(row);
// Initialize Map after DOM insertion
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);
// Use CartoDB Dark Matter tiles to match the ChatGPT dark theme natively
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
attribution: '© OpenStreetMap © CARTO',
subdomains: 'abcd',
maxZoom: 20
}).addTo(map);
const marker = L.marker([lat, lon]).addTo(map);
marker.bindPopup(`${title}`).openPopup();
// Invalidate size to ensure it renders correctly in a dynamic container
setTimeout(() => map.invalidateSize(), 300);
}
function scrollToBottom() {
chatContainer.scrollTop = chatContainer.scrollHeight;
}
// =====================================
// GLOBAL MAP MODAL LOGIC
// =====================================
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';
// Initialize map only once
if (!globalLeafletMap) {
globalLeafletMap = L.map('global-map').setView([34.88, -1.30], 13); // Default view
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
attribution: '© OpenStreetMap',
subdomains: 'abcd',
maxZoom: 20
}).addTo(globalLeafletMap);
}
// Must invalidate size since it was display:none
setTimeout(() => globalLeafletMap.invalidateSize(), 300);
// Fetch all services
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);
// Action button inside popup to search the service directly
const popupContent = `
${srv.service}
`;
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';
}
});
});