Chatm / templates /index.html
Docfile's picture
Update templates/index.html
a5874c3 verified
raw
history blame
30.1 kB
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Assistant IA</title>
<!-- Using Tailwind via CDN -->
<script src="https://cdn.tailwindcss.com?plugins=forms,typography"></script>
<!-- Using Marked.js via CDN -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
:root {
--primary-blue: #1E40AF; /* blue-800 */
--light-blue: #EFF6FF; /* blue-50 */
--accent-blue: #3B82F6; /* blue-500 */
--text-dark: #111827; /* gray-900 */
--text-light: #4B5563; /* gray-600 */
--bg-user: #2563EB; /* blue-600 */
--bg-assistant: #FFFFFF;
--border-color: #E5E7EB; /* gray-200 */
}
body {
background: linear-gradient(135deg, var(--light-blue) 0%, #ffffff 100%);
min-height: 100vh;
font-family: 'Inter', sans-serif; /* Consider adding Inter font via Google Fonts */
}
/* Enhanced Glass Effect */
.glass-effect {
background: rgba(255, 255, 255, 0.8); /* Slightly less transparent */
backdrop-filter: blur(12px) saturate(180%); /* Increased blur and saturation */
-webkit-backdrop-filter: blur(12px) saturate(180%);
border: 1px solid rgba(230, 230, 230, 0.3); /* Softer border */
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.15); /* Softer shadow */
}
/* Message Styling */
.message-container {
display: flex;
margin-bottom: 1rem;
opacity: 0;
transform: translateY(10px);
animation: fadeIn 0.5s ease forwards;
}
@keyframes fadeIn {
to {
opacity: 1;
transform: translateY(0);
}
}
.message-bubble {
max-width: 75%;
padding: 0.75rem 1rem; /* 12px 16px */
border-radius: 1rem; /* large */
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
word-wrap: break-word; /* Ensure long words break */
overflow-wrap: break-word; /* Modern standard */
}
.message-container.user {
justify-content: flex-end;
}
.message-container.assistant {
justify-content: flex-start;
}
.message-bubble.user {
background-color: var(--bg-user);
color: white;
border-bottom-right-radius: 0.25rem; /* small */
}
.message-bubble.assistant {
background-color: var(--bg-assistant);
color: var(--text-dark);
border: 1px solid var(--border-color);
border-bottom-left-radius: 0.25rem; /* small */
}
/* Styling for code blocks generated by marked.js */
.message-bubble pre {
background-color: #f3f4f6; /* gray-100 */
color: #1f2937; /* gray-800 */
padding: 1em;
border-radius: 0.5rem; /* md */
overflow-x: auto;
font-family: 'Courier New', Courier, monospace;
font-size: 0.9em;
margin: 0.5em 0;
}
.message-bubble code:not(pre code) { /* Inline code */
background-color: #e5e7eb; /* gray-200 */
padding: 0.2em 0.4em;
border-radius: 0.25rem;
font-size: 0.9em;
}
.message-bubble ul, .message-bubble ol {
padding-left: 1.5rem;
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
.message-bubble li > p { /* Prevent extra margins inside list items */
margin-bottom: 0.25rem;
}
.message-bubble blockquote {
border-left: 4px solid var(--accent-blue);
padding-left: 1rem;
margin-left: 0;
color: var(--text-light);
font-style: italic;
}
/* Quick Actions */
.quick-action {
background: linear-gradient(135deg, var(--primary-blue) 0%, var(--accent-blue) 100%);
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
color: white;
font-weight: 500;
}
.quick-action:hover {
transform: translateY(-3px) scale(1.03);
box-shadow: 0 6px 15px rgba(59, 130, 246, 0.35);
}
.quick-action:active {
transform: translateY(-1px) scale(0.98);
box-shadow: 0 3px 8px rgba(59, 130, 246, 0.25);
}
.quick-action svg {
transition: transform 0.2s ease-in-out;
}
.quick-action:hover svg {
transform: rotate(-5deg) scale(1.1);
}
/* Input Area */
.custom-input {
border: 2px solid var(--border-color);
transition: all 0.3s ease;
color: var(--text-dark);
padding-right: 3rem; /* Space for potential icons inside input */
}
.custom-input::placeholder {
color: #9ca3af; /* gray-400 */
}
.custom-input:focus {
border-color: var(--accent-blue);
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.1);
outline: none;
}
/* Send Button */
#sendButton {
/* Inherits quick-action styles */
min-width: 80px; /* Ensure button doesn't shrink too much */
}
/* Loading Spinner */
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-spinner {
border: 4px solid rgba(59, 130, 246, 0.2); /* Lighter border */
border-top-color: var(--accent-blue); /* Spinner color */
animation: spin 1s linear infinite;
}
#loadingOverlay {
transition: opacity 0.3s ease-in-out;
}
/* File Upload Preview */
.file-upload-area {
background: rgba(59, 130, 246, 0.05);
border: 2px dashed var(--accent-blue);
transition: all 0.3s ease;
}
.file-upload-area:hover {
background: rgba(59, 130, 246, 0.1);
border-color: var(--primary-blue);
}
#fileName {
font-size: 0.9rem;
font-style: italic;
}
/* Scrollbar */
#chatMessages::-webkit-scrollbar {
width: 8px;
}
#chatMessages::-webkit-scrollbar-track {
background: var(--light-blue);
border-radius: 10px;
}
#chatMessages::-webkit-scrollbar-thumb {
background-color: var(--accent-blue);
border-radius: 10px;
border: 2px solid var(--light-blue);
}
#chatMessages {
scrollbar-width: thin;
scrollbar-color: var(--accent-blue) var(--light-blue);
}
</style>
</head>
<body class="text-gray-800">
<div class="container mx-auto px-4 py-8 max-w-4xl"> <!-- Centered & Max Width -->
<!-- Header -->
<header class="glass-effect rounded-2xl p-4 md:p-6 mb-6 flex flex-col sm:flex-row justify-between items-center">
<div class="flex items-center space-x-3 mb-4 sm:mb-0">
<div class="w-12 h-12 rounded-full bg-gradient-to-br from-blue-600 to-blue-800 flex items-center justify-center shadow-md">
<svg class="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"></path>
</svg>
</div>
<div>
<h1 class="text-xl md:text-2xl font-bold text-blue-900">Assistant IA</h1>
<p class="text-sm text-green-600 font-medium">En ligne</p>
</div>
</div>
<div class="flex items-center space-x-3">
<label class="flex items-center space-x-2 text-sm text-blue-900 cursor-pointer hover:text-blue-700">
<input type="checkbox" id="webSearchToggle" class="form-checkbox h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
<span>Recherche web</span>
</label>
<button id="clearChatButton" title="Effacer la conversation" class="bg-red-500 hover:bg-red-600 text-white px-3 py-2 rounded-lg transition-colors duration-200 text-sm font-medium flex items-center space-x-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>
<span>Effacer</span>
</button>
</div>
</header>
<!-- Quick Actions (Optional: Keep or remove based on preference) -->
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 gap-3 mb-6">
<!-- Example Quick Action Button -->
<button data-prompt="Effectuez une recherche sur " class="quick-action p-3 rounded-xl flex flex-col sm:flex-row items-center justify-center space-y-1 sm:space-y-0 sm:space-x-2 text-xs sm:text-sm">
<svg class="w-5 h-5 mb-1 sm:mb-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
<span>Recherche</span>
</button>
<button data-prompt="Donnez-moi des idées sur " class="quick-action p-3 rounded-xl flex flex-col sm:flex-row items-center justify-center space-y-1 sm:space-y-0 sm:space-x-2 text-xs sm:text-sm">
<svg class="w-5 h-5 mb-1 sm:mb-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
<span>Brainstorm</span>
</button>
<button data-prompt="Analysez les données suivantes : " class="quick-action p-3 rounded-xl flex flex-col sm:flex-row items-center justify-center space-y-1 sm:space-y-0 sm:space-x-2 text-xs sm:text-sm">
<svg class="w-5 h-5 mb-1 sm:mb-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/></svg>
<span>Analyse</span>
</button>
<button data-prompt="Créez une image de " class="quick-action p-3 rounded-xl flex flex-col sm:flex-row items-center justify-center space-y-1 sm:space-y-0 sm:space-x-2 text-xs sm:text-sm">
<svg class="w-5 h-5 mb-1 sm:mb-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
<span>Images</span>
</button>
<button data-prompt="Écrivez du code pour " class="quick-action p-3 rounded-xl flex flex-col sm:flex-row items-center justify-center space-y-1 sm:space-y-0 sm:space-x-2 text-xs sm:text-sm">
<svg class="w-5 h-5 mb-1 sm:mb-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/></svg>
<span>Code</span>
</button>
</div>
<!-- Chat Messages -->
<div id="chatMessages" class="glass-effect rounded-2xl p-4 md:p-6 mb-6 h-[50vh] md:h-[60vh] overflow-y-auto space-y-4">
<!-- Messages Rendered by Server (Jinja2) -->
{% for message in messages %}
<div class="message-container {{ message.role }}"> {# Use role for class #}
<div class="message-bubble {{ message.role }}">
{# Use marked.parse and sanitize output #}
{{ message.content | safe }} {# Assume backend sends sanitized HTML or use JS #}
</div>
</div>
{% endfor %}
<!-- New messages will be appended here by JavaScript -->
</div>
<!-- Input Area -->
<div class="glass-effect rounded-2xl p-4 md:p-6">
<!-- File Upload Area -->
<div class="mb-4">
<input type="file" id="fileUpload" class="hidden" accept=".jpg,.jpeg,.png,.gif,.webp,.heic,.heif,.pdf,.txt,.mp3,.wav,.ogg,.mp4,.mov,.avi">
<label for="fileUpload" class="file-upload-area p-4 rounded-xl flex flex-col items-center justify-center cursor-pointer hover:bg-blue-50 text-center">
<svg class="w-8 h-8 text-blue-500 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"></path>
</svg>
<span id="fileName" class="text-blue-600 text-sm">Cliquez ou déposez un fichier ici (Max 100Mo)</span>
<span id="uploadStatus" class="text-xs text-gray-500 mt-1"></span>
</label>
</div>
<!-- Text Input & Send Button -->
<div class="flex items-center space-x-3">
<div class="relative flex-grow">
<textarea id="messageInput"
class="custom-input w-full rounded-xl px-4 py-3 pr-12 text-sm md:text-base resize-none border-gray-300 focus:border-blue-500 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
placeholder="Écrivez votre message... (Shift+Enter pour nouvelle ligne)"
rows="1"
style="min-height: 48px; max-height: 150px;"
oninput="this.style.height = 'auto'; this.style.height = (this.scrollHeight) + 'px';"
></textarea>
</div>
<button id="sendButton" title="Envoyer le message" class="quick-action px-4 py-3 rounded-xl flex items-center justify-center self-end transition-transform duration-150 ease-in-out">
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"></path></svg>
<span class="sr-only">Envoyer</span>
</button>
</div>
</div>
<footer class="text-center mt-6 text-xs text-gray-500">
Propulsé par Gemini | Conçu par Youssouf
</footer>
</div>
<!-- Loading Overlay -->
<div id="loadingOverlay" class="fixed inset-0 bg-white bg-opacity-75 backdrop-filter backdrop-blur-sm hidden items-center justify-center z-50" style="display: none;">
<div class="loading-spinner w-16 h-16 rounded-full"></div>
<span id="loadingText" class="ml-4 text-blue-800 font-medium">Chargement...</span>
</div>
<script>
const messageInput = document.getElementById('messageInput');
const chatMessages = document.getElementById('chatMessages');
const webSearchToggle = document.getElementById('webSearchToggle');
const fileUpload = document.getElementById('fileUpload');
const fileNameLabel = document.getElementById('fileName');
const uploadStatusLabel = document.getElementById('uploadStatus');
const loadingOverlay = document.getElementById('loadingOverlay');
const loadingText = document.getElementById('loadingText');
const sendButton = document.getElementById('sendButton');
const clearChatButton = document.getElementById('clearChatButton');
let currentFileName = null; // Track uploaded file name
// --- Utility Functions ---
function showLoading(text = "Chargement...") {
loadingText.textContent = text;
loadingOverlay.style.display = 'flex'; // Use style.display
loadingOverlay.style.opacity = 1;
}
function hideLoading() {
// Add a small delay for smoother transition if needed
loadingOverlay.style.opacity = 0;
setTimeout(() => {
loadingOverlay.style.display = 'none';
}, 300); // Match CSS transition duration
}
function sanitizeHTML(str) {
// Basic sanitizer (consider a more robust library like DOMPurify for production)
const temp = document.createElement('div');
temp.textContent = str;
return temp.innerHTML;
}
function addMessage(content, role = 'assistant') {
const messageContainer = document.createElement('div');
messageContainer.className = `message-container ${role}`; // 'user' or 'assistant'
const messageBubble = document.createElement('div');
messageBubble.className = `message-bubble ${role}`;
// Use marked.parse for Markdown rendering
// Ensure marked is loaded before this script runs
if (typeof marked === 'undefined') {
console.error("Marked.js library is not loaded.");
messageBubble.textContent = content; // Fallback to plain text
} else {
// Basic sanitization - FOR PRODUCTION, USE A ROBUST LIBRARY (e.g., DOMPurify)
// marked.parse might return HTML, ensure it's safe before inserting
const rawHtml = marked.parse(content);
// Simple placeholder for sanitization concept:
// const sanitizedHtml = yourSanitizationFunction(rawHtml);
// For now, we'll trust the backend/marked output for this example, but BE CAREFUL.
messageBubble.innerHTML = rawHtml; // Potentially unsafe if content isn't trusted
}
messageContainer.appendChild(messageBubble);
chatMessages.appendChild(messageContainer);
// Smooth scroll to the bottom
chatMessages.scrollTo({ top: chatMessages.scrollHeight, behavior: 'smooth' });
}
// --- Event Listeners ---
sendButton.addEventListener('click', sendMessage);
messageInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault(); // Prevent default Enter behavior (new line)
sendMessage();
}
});
clearChatButton.addEventListener('click', clearChat);
fileUpload.addEventListener('change', handleFileUpload);
document.querySelectorAll('.quick-action[data-prompt]').forEach(button => {
button.addEventListener('click', () => {
const promptText = button.getAttribute('data-prompt');
messageInput.value = promptText;
messageInput.focus();
// Auto-resize textarea after setting value
messageInput.style.height = 'auto';
messageInput.style.height = (messageInput.scrollHeight) + 'px';
// Optional: Place cursor at the end
messageInput.setSelectionRange(promptText.length, promptText.length);
});
});
// --- Core Functions ---
async function sendMessage() {
const messageText = messageInput.value.trim();
if (!messageText && !currentFileName) { // Don't send empty messages unless a file is context
console.log("Empty message, not sending.");
return;
}
// Add user message optimistically to UI
// Use the raw text, backend response will be formatted
addMessage(sanitizeHTML(messageText), 'user');
const userMessageToSend = messageText; // Keep the original text for sending
messageInput.value = ''; // Clear input
messageInput.style.height = '48px'; // Reset textarea height
showLoading("Envoi en cours...");
try {
const response = await fetch('/send_message', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json' // Explicitly accept JSON
},
body: JSON.stringify({
message: userMessageToSend,
web_search: webSearchToggle.checked
// File info is now handled server-side via session
})
});
hideLoading(); // Hide loading indicator once response headers are received
if (!response.ok) {
// Try to get error message from JSON response body
let errorMsg = `Erreur HTTP: ${response.status}`;
try {
const errorData = await response.json();
errorMsg = `Erreur: ${errorData.error || response.statusText}`;
} catch (jsonError) {
// If response is not JSON, use statusText
console.warn("Could not parse error response as JSON:", jsonError);
}
addMessage(errorMsg, 'assistant'); // Display error in chat
console.error("Send message error:", errorMsg);
return; // Stop processing on error
}
const data = await response.json();
if (data.error) {
addMessage(`Erreur: ${sanitizeHTML(data.error)}`, 'assistant');
} else if (data.response) {
addMessage(data.response, 'assistant'); // Add AI response (already Markdown processed if backend does it, or process here)
// If files were processed, reset the file indicator?
// Decide based on whether files persist across messages
// resetFileUploadUI(); // Uncomment if needed
} else {
addMessage("Réponse inattendue du serveur.", 'assistant');
}
} catch (error) {
hideLoading();
console.error('Erreur lors de l\'envoi du message:', error);
addMessage(`Erreur de connexion ou de traitement: ${sanitizeHTML(error.message)}`, 'assistant');
}
}
async function handleFileUpload(event) {
const file = event.target.files[0];
if (!file) return;
// Basic validation (optional: add more checks like type if needed)
const maxSize = 100 * 1024 * 1024; // 100MB
if (file.size > maxSize) {
uploadStatusLabel.textContent = `Fichier trop volumineux (Max ${maxSize / 1024 / 1024}Mo).`;
uploadStatusLabel.style.color = 'red';
fileUpload.value = ''; // Clear the file input
return;
}
fileNameLabel.textContent = `Fichier sélectionné : ${file.name}`;
uploadStatusLabel.textContent = 'Upload en cours...';
uploadStatusLabel.style.color = 'gray';
showLoading("Téléversement du fichier...");
const formData = new FormData();
formData.append('file', file);
try {
const response = await fetch('/upload', {
method: 'POST',
body: formData
// No 'Content-Type' header needed, browser sets it for FormData
});
hideLoading();
const data = await response.json();
if (!response.ok || data.error) {
const errorMsg = data.error || `Erreur HTTP ${response.status}`;
uploadStatusLabel.textContent = `Échec: ${sanitizeHTML(errorMsg)}`;
uploadStatusLabel.style.color = 'red';
fileNameLabel.textContent = 'Cliquez ou déposez un fichier ici (Max 100Mo)'; // Reset label
fileUpload.value = ''; // Clear the file input
currentFileName = null;
console.error("File upload error:", errorMsg);
} else if (data.success) {
uploadStatusLabel.textContent = 'Prêt à être envoyé avec votre message.';
uploadStatusLabel.style.color = 'green';
currentFileName = data.filename; // Store filename indicating success
// Optionally add a message to chat confirming upload?
// addMessage(`Fichier "${sanitizeHTML(data.filename)}" prêt.`, 'system'); // Example system message
} else {
uploadStatusLabel.textContent = 'Réponse serveur inattendue.';
uploadStatusLabel.style.color = 'red';
currentFileName = null;
}
} catch (error) {
hideLoading();
console.error('Erreur lors de l\'upload:', error);
uploadStatusLabel.textContent = `Erreur: ${sanitizeHTML(error.message)}`;
uploadStatusLabel.style.color = 'red';
fileNameLabel.textContent = 'Cliquez ou déposez un fichier ici (Max 100Mo)'; // Reset label
fileUpload.value = ''; // Clear the file input
currentFileName = null;
}
}
async function clearChat() {
if (!confirm("Êtes-vous sûr de vouloir effacer toute la conversation et les fichiers uploadés ?")) {
return;
}
showLoading("Effacement...");
try {
const response = await fetch('/clear_chat', { method: 'POST' });
hideLoading();
if (!response.ok) {
throw new Error(`Erreur serveur: ${response.status}`);
}
const data = await response.json();
if (data.success) {
chatMessages.innerHTML = ''; // Clear messages on frontend
resetFileUploadUI();
messageInput.value = ''; // Clear input field
messageInput.style.height = '48px'; // Reset textarea height
console.log("Chat cleared successfully.");
// Optionally add a confirmation message
// addMessage("Conversation effacée.", "system");
} else {
throw new Error("Le serveur n'a pas confirmé l'effacement.");
}
} catch (error) {
hideLoading();
console.error('Erreur lors de l\'effacement:', error);
// Maybe display error in a non-chat way (e.g., alert or status bar)
alert(`Erreur lors de l'effacement : ${error.message}`);
}
}
function resetFileUploadUI() {
fileNameLabel.textContent = 'Cliquez ou déposez un fichier ici (Max 100Mo)';
uploadStatusLabel.textContent = '';
fileUpload.value = ''; // Clear the actual file input
currentFileName = null;
}
// --- Initial Setup ---
// Scroll to bottom on initial load (after server-rendered messages)
window.addEventListener('load', () => {
chatMessages.scrollTop = chatMessages.scrollHeight;
// Initial resize check for text area in case of cached content
messageInput.style.height = 'auto';
messageInput.style.height = (messageInput.scrollHeight) + 'px';
messageInput.focus(); // Focus input on load
// Initial rendering of messages from server uses Jinja template.
// We need to apply marked AFTER the page loads to the server-rendered content.
document.querySelectorAll('.message-bubble').forEach(bubble => {
// Re-parse content that came from the server template if it wasn't pre-rendered as HTML
// This assumes the server sent raw markdown in message.content
// If the server sends pre-rendered HTML via `| safe`, this step might not be needed
// or could cause double-rendering issues. Adjust based on backend output.
// Let's assume backend sends raw markdown for consistency with JS added messages:
if (typeof marked !== 'undefined') {
const currentContent = bubble.innerHTML; // Get the raw content from template
bubble.innerHTML = marked.parse(currentContent); // Parse and replace
}
});
});
// Optional: Add online/offline status indicators
window.addEventListener('offline', () => addMessage('⚠️ Vous êtes hors ligne. Vérifiez votre connexion.', 'system'));
window.addEventListener('online', () => addMessage('✅ Vous êtes de nouveau en ligne.', 'system'));
</script>
</body>
</html>