Space07 / static /script.js
Hakim18's picture
Upload 10 files
6fd7d0c verified
Raw
History Blame Contribute Delete
16.2 kB
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'
? '<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>';
// Format basic markdown-like text elements (newlines)
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';
// 1. Add Answer Text
const textP = document.createElement('p');
textP.innerHTML = (data.response || '').replace(/\n/g, '<br>');
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 = `
<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);
}
// 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 = `
<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);
// 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: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> &copy; <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();
// 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: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
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 = `
<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';
}
});
});