Chatm / templates /index.html
Docfile's picture
Update templates/index.html
605d3e8 verified
raw
history blame
30.1 kB
<!DOCTYPE html>
<html lang="fr" class="">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mariam AI - Assistant Personnel</title>
<!-- Tailwind CSS via CDN -->
<script src="https://cdn.tailwindcss.com?plugins=forms,typography"></script>
<!-- Font Awesome pour les icônes -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<!-- Favicon -->
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>✨</text></svg>">
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
mono: ['"JetBrains Mono"', 'monospace']
},
colors: {
primary: { 50: '#eff6ff', 100: '#dbeafe', 200: '#bfdbfe', 300: '#93c5fd', 400: '#60a5fa', 500: '#3b82f6', 600: '#2563eb', 700: '#1d4ed8', 800: '#1e40af', 900: '#1e3a8a' },
slate: { 50: '#f8fafc', 100: '#f1f5f9', 200: '#e2e8f0', 300: '#cbd5e1', 400: '#94a3b8', 500: '#64748b', 600: '#475569', 700: '#334155', 800: '#1e293b', 900: '#0f172a', 950: '#020617' }
},
animation: {
'fade-in': 'fadeIn 0.5s ease-out forwards',
'fade-in-up': 'fadeInUp 0.5s ease-out forwards',
'pulse-subtle': 'pulse 2.5s cubic-bezier(0.4, 0, 0.6, 1) infinite',
},
keyframes: {
fadeIn: { '0%': { opacity: 0 }, '100%': { opacity: 1 } },
fadeInUp: { '0%': { opacity: 0, transform: 'translateY(10px)' }, '100%': { opacity: 1, transform: 'translateY(0)' } },
}
}
}
}
</script>
<style>
:root {
color-scheme: light;
}
:root.dark {
color-scheme: dark;
}
html, body {
height: 100%;
overflow: hidden; /* Prevent scrollbars on body due to fixed elements */
}
body {
font-family: 'Inter', sans-serif;
background: #f8fafc; /* light mode body bg */
color: #0f172a; /* light mode text */
}
.dark body {
background: #0f172a; /* dark mode body bg */
color: #f1f5f9; /* dark mode text */
}
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 6px; }
.dark ::-webkit-scrollbar-thumb { background: #475569; }
::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
.dark ::-webkit-scrollbar-thumb:hover { background: #64748b; }
/* Layout */
.main-layout { display: flex; height: 100vh; }
/* sidebar width for mobile is w-64, for desktop min-w-[260px] */
.chat-container { display: flex; flex-direction: column; height: 100%; }
/* Welcome Screen */
.suggestion-card {
transition: all 0.2s ease-in-out;
border: 1px solid #e2e8f0; /* light: border-slate-200 */
}
.dark .suggestion-card { border-color: #334155; } /* dark: border-slate-700 */
.suggestion-card:hover {
transform: translateY(-4px);
box-shadow: 0 4px 10px rgba(0,0,0,0.05);
border-color: #93c5fd; /* light: border-primary-300 */
}
.dark .suggestion-card:hover {
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
border-color: #3b82f6; /* dark: border-primary-500 */
}
/* Messages */
.message-bubble {
max-width: 80%; /* Max width for bubbles on mobile and desktop */
padding: 0.75rem 1.125rem;
border-radius: 1.25rem;
animation: fadeInUp 0.4s ease-out;
}
.user-message .message-bubble {
background-color: #2563eb; /* primary-600 */
color: white;
border-bottom-right-radius: 0.25rem;
}
.assistant-message .message-bubble {
background-color: #ffffff; /* light: white */
color: #1e293b; /* light: slate-800 */
border: 1px solid #e2e8f0; /* light: slate-200 */
border-bottom-left-radius: 0.25rem;
}
.dark .assistant-message .message-bubble {
background-color: #1e293b; /* dark: slate-800 */
color: #e2e8f0; /* dark: slate-200 */
border-color: #334155; /* dark: slate-700 */
}
.assistant-avatar {
background: linear-gradient(135deg, #3b82f6, #9333ea);
}
/* Input Area */
.chat-input-area {
resize: none;
min-height: 52px; /* Fixed based on py-3 and text size */
max-height: 200px; /* Reduced max height slightly */
}
.toast {
animation: toast-in-out 5s ease-in-out forwards;
}
@keyframes toast-in-out {
0%, 100% { transform: translateY(200%); opacity: 0; }
10%, 90% { transform: translateY(0); opacity: 1; }
}
pre code.hljs{padding:1em;border-radius:.5rem}
.prose table{width:100%;border-collapse:collapse}.prose td,.prose th{border:1px solid #e2e8f0;padding:.5rem .75rem;text-align:left}.dark .prose td,.dark .prose th{border-color:#334155}.prose thead{background-color:#f8fafc;font-weight:600}.dark .prose thead{background-color:#1e293b}.prose tbody tr:nth-child(2n){background-color:#f8fafc}.dark .prose tbody tr:nth-child(2n){background-color:#1e293b}
</style>
</head>
<body class="antialiased">
<div class="main-layout bg-slate-50 dark:bg-slate-950">
<!-- Sidebar -->
<aside id="sidebar" class="fixed inset-y-0 left-0 z-40 w-64 bg-slate-100 dark:bg-slate-900/70 backdrop-blur-md dark:backdrop-blur-sm border-r border-slate-200 dark:border-slate-800 flex flex-col p-4 transform -translate-x-full transition-transform duration-300 ease-in-out md:relative md:translate-x-0 md:w-auto md:min-w-[260px] md:z-auto md:backdrop-blur-none">
<div class="flex items-center justify-between mb-6">
<div class="flex items-center gap-2">
<img src="https://mariam-241.vercel.app/static/image/logoboma.png" alt="Logo" class="h-8">
<h1 class="text-xl font-bold text-slate-800 dark:text-slate-100">Mariam AI</h1>
</div>
<button id="close-sidebar-btn" class="p-1 rounded-md md:hidden text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200">
<i class="fa-solid fa-times text-xl"></i>
</button>
</div>
<button id="new-chat-btn" class="w-full flex items-center justify-center gap-2 px-4 py-2.5 mb-4 text-sm font-semibold text-white bg-primary-600 rounded-lg hover:bg-primary-700 transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:focus:ring-offset-slate-900">
<i class="fa-solid fa-plus"></i>
Nouvelle Conversation
</button>
<div class="flex-grow overflow-y-auto">
<!-- Future chat history list -->
</div>
<div class="border-t border-slate-200 dark:border-slate-800 pt-4 mt-auto space-y-2">
<button id="theme-toggle" class="w-full flex items-center gap-3 px-3 py-2 text-sm font-medium text-slate-600 dark:text-slate-400 hover:bg-slate-200 dark:hover:bg-slate-800 rounded-md transition-colors">
<i class="fa-solid fa-sun w-4 text-center dark:hidden"></i>
<i class="fa-solid fa-moon w-4 text-center hidden dark:inline-block"></i>
<span>Changer de thème</span>
</button>
</div>
</aside>
<!-- Overlay for mobile sidebar -->
<div id="sidebar-overlay" class="fixed inset-0 z-30 bg-black/30 backdrop-blur-sm hidden md:hidden"></div>
<!-- Main Chat Area -->
<main class="flex-1 chat-container">
<!-- Chat Header -->
<header class="flex items-center justify-between p-3 sm:p-4 border-b border-slate-200 dark:border-slate-800">
<button id="sidebar-toggle" class="p-2 rounded-md md:hidden hover:bg-slate-100 dark:hover:bg-slate-800">
<i class="fa-solid fa-bars text-slate-600 dark:text-slate-400"></i>
</button>
<h2 class="text-base sm:text-lg font-semibold text-slate-700 dark:text-slate-300 mx-auto md:mx-0">Conversation Actuelle</h2>
<div class="flex items-center gap-2">
<label class="flex items-center cursor-pointer group">
<span class="mr-1.5 sm:mr-2 text-xs sm:text-sm font-medium text-slate-600 dark:text-slate-400">Web</span>
<div class="relative">
<input type="checkbox" id="web_search_toggle" class="sr-only peer">
<div class="w-9 h-5 sm:w-10 sm:h-5 bg-slate-300 dark:bg-slate-700 rounded-full peer peer-checked:after:translate-x-[calc(100%-2px)] peer-checked:after:border-white after:content-[''] after:absolute after:top-[1px] sm:after:top-0.5 after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-primary-500"></div>
</div>
</label>
</div>
</header>
<!-- Messages -->
<div id="chat-messages" class="flex-1 overflow-y-auto p-4 sm:p-6 space-y-4 sm:space-y-6">
<!-- Welcome Screen -->
<div id="welcome-screen" class="flex flex-col items-center justify-center h-full text-center animate-fade-in">
<div class="assistant-avatar w-16 h-16 sm:w-20 sm:h-20 rounded-full flex items-center justify-center mb-4 text-3xl sm:text-4xl text-white animate-pulse-subtle"></div>
<h1 class="text-2xl sm:text-3xl font-bold text-slate-800 dark:text-slate-100 mb-2">Comment puis-je vous aider ?</h1>
<p class="text-slate-500 dark:text-slate-400 mb-8 sm:mb-10 max-w-md text-sm sm:text-base">Je suis Mariam, un assistant IA capable de répondre à vos questions, de générer du code, et bien plus encore.</p>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4 w-full max-w-2xl">
<div class="suggestion-card bg-white dark:bg-slate-800 p-3 sm:p-4 rounded-lg cursor-pointer" data-prompt="Donne-moi 3 idées de recettes rapides pour le dîner de ce soir.">
<p class="font-semibold text-slate-700 dark:text-slate-200 text-sm sm:text-base">Idées de recettes</p>
<p class="text-xs sm:text-sm text-slate-500 dark:text-slate-400">Pour un repas rapide et délicieux</p>
</div>
<div class="suggestion-card bg-white dark:bg-slate-800 p-3 sm:p-4 rounded-lg cursor-pointer" data-prompt="Écris un e-mail professionnel pour demander un jour de congé.">
<p class="font-semibold text-slate-700 dark:text-slate-200 text-sm sm:text-base">Rédiger un e-mail</p>
<p class="text-xs sm:text-sm text-slate-500 dark:text-slate-400">Pour une demande de congé</p>
</div>
<div class="suggestion-card bg-white dark:bg-slate-800 p-3 sm:p-4 rounded-lg cursor-pointer" data-prompt="Explique le concept de 'machine learning' en termes simples.">
<p class="font-semibold text-slate-700 dark:text-slate-200 text-sm sm:text-base">Expliquer un concept</p>
<p class="text-xs sm:text-sm text-slate-500 dark:text-slate-400">Le machine learning pour les débutants</p>
</div>
<div class="suggestion-card bg-white dark:bg-slate-800 p-3 sm:p-4 rounded-lg cursor-pointer" data-prompt="Écris une fonction Python qui inverse une chaîne de caractères.">
<p class="font-semibold text-slate-700 dark:text-slate-200 text-sm sm:text-base">Générer du code</p>
<p class="text-xs sm:text-sm text-slate-500 dark:text-slate-400">Pour inverser une chaîne en Python</p>
</div>
</div>
</div>
<!-- Dynamic messages will be injected here -->
<div id="history-loading" class="text-center py-10 hidden">
<i class="fa-solid fa-spinner fa-spin text-2xl text-primary-500"></i>
<p class="mt-2 text-sm text-slate-500">Chargement de la conversation...</p>
</div>
<div id="loading-indicator" class="assistant-message flex items-start gap-3 hidden">
<div class="assistant-avatar w-8 h-8 sm:w-9 sm:h-9 rounded-full flex items-center justify-center flex-shrink-0 text-lg sm:text-xl text-white"></div>
<div class="message-bubble flex items-center gap-2">
<div class="h-2 w-2 bg-slate-400 rounded-full animate-bounce [animation-delay:-0.3s]"></div>
<div class="h-2 w-2 bg-slate-400 rounded-full animate-bounce [animation-delay:-0.15s]"></div>
<div class="h-2 w-2 bg-slate-400 rounded-full animate-bounce"></div>
</div>
</div>
</div>
<!-- Chat Input Area -->
<footer class="p-3 sm:p-4 bg-white/80 dark:bg-slate-900/80 backdrop-blur-sm">
<div class="max-w-4xl mx-auto">
<div id="preview-area" class="px-2 py-1.5 sm:px-4 sm:py-2 hidden"></div>
<form id="chat-form" class="relative">
<textarea id="prompt" name="prompt" rows="1" class="chat-input-area w-full bg-slate-100 dark:bg-slate-800 border-2 border-transparent focus:border-primary-500 focus:ring-0 rounded-xl py-3 pl-10 pr-[48px] sm:pl-12 sm:pr-[56px] text-sm sm:text-base text-slate-800 dark:text-slate-100 transition-colors duration-200" placeholder="Envoyez un message..."></textarea>
<div class="absolute left-2 sm:left-3 top-1/2 -translate-y-1/2 flex items-center">
<label for="file_upload" class="cursor-pointer text-slate-500 hover:text-primary-500 transition-colors p-1">
<i class="fa-solid fa-paperclip text-base sm:text-lg"></i>
<input type="file" id="file_upload" name="file" class="hidden" accept=".txt,.pdf,.png,.jpg,.jpeg,.md,.py,.js,.html,.css,.json">
</label>
</div>
<button type="submit" id="send-button" class="absolute right-2 sm:right-3 top-1/2 -translate-y-1/2 w-8 h-8 sm:w-9 sm:h-9 bg-primary-600 text-white rounded-full flex items-center justify-center hover:bg-primary-700 disabled:bg-slate-300 dark:disabled:bg-slate-700 disabled:cursor-not-allowed transition-all">
<i class="fa-solid fa-arrow-up text-sm sm:text-base"></i>
</button>
</form>
<p class="text-xs text-center text-slate-400 dark:text-slate-500 mt-2">
Mariam AI peut faire des erreurs. Vérifiez les informations importantes.
</p>
</div>
</footer>
</main>
</div>
<!-- Toast/Error Notification Area -->
<div id="toast-container" class="fixed bottom-4 right-4 sm:bottom-5 sm:right-5 z-50 w-[calc(100%-2rem)] max-w-xs sm:max-w-sm"></div>
<script>
document.addEventListener('DOMContentLoaded', () => {
// --- DOM Elements ---
const dom = {
sidebar: document.getElementById('sidebar'),
sidebarToggle: document.getElementById('sidebar-toggle'),
closeSidebarBtn: document.getElementById('close-sidebar-btn'),
sidebarOverlay: document.getElementById('sidebar-overlay'),
themeToggle: document.getElementById('theme-toggle'),
newChatBtn: document.getElementById('new-chat-btn'),
chatForm: document.getElementById('chat-form'),
promptInput: document.getElementById('prompt'),
sendButton: document.getElementById('send-button'),
chatMessages: document.getElementById('chat-messages'),
welcomeScreen: document.getElementById('welcome-screen'),
historyLoading: document.getElementById('history-loading'),
loadingIndicator: document.getElementById('loading-indicator'),
webSearchToggle: document.getElementById('web_search_toggle'),
fileUpload: document.getElementById('file_upload'),
previewArea: document.getElementById('preview-area'),
toastContainer: document.getElementById('toast-container'),
suggestionCards: document.querySelectorAll('.suggestion-card')
};
// --- State ---
let isComposing = false;
let currentFile = null;
// --- API Endpoints ---
const API_CHAT_ENDPOINT = '/api/chat';
const API_HISTORY_ENDPOINT = '/api/history';
const CLEAR_ENDPOINT = '/clear';
// --- UI Functions ---
const setUIState = (isLoading) => {
dom.promptInput.disabled = isLoading;
dom.sendButton.disabled = isLoading || (!dom.promptInput.value.trim() && !currentFile);
dom.fileUpload.disabled = isLoading;
if (isLoading) {
dom.loadingIndicator.classList.remove('hidden');
scrollToBottom();
} else {
dom.loadingIndicator.classList.add('hidden');
}
};
const adjustTextareaHeight = () => {
dom.promptInput.style.height = 'auto';
let newHeight = dom.promptInput.scrollHeight;
const maxHeight = parseInt(window.getComputedStyle(dom.promptInput).maxHeight, 10);
if (newHeight > maxHeight) {
newHeight = maxHeight;
dom.promptInput.style.overflowY = 'auto';
} else {
dom.promptInput.style.overflowY = 'hidden';
}
dom.promptInput.style.height = `${newHeight}px`;
updateSendButtonState();
};
const updateSendButtonState = () => {
const hasText = dom.promptInput.value.trim().length > 0;
const hasFile = !!currentFile;
dom.sendButton.disabled = !hasText && !hasFile;
};
const scrollToBottom = (behavior = 'smooth') => {
setTimeout(() => {
dom.chatMessages.scrollTo({ top: dom.chatMessages.scrollHeight, behavior });
}, 50);
};
const openMobileSidebar = () => {
dom.sidebar.classList.remove('-translate-x-full');
dom.sidebarOverlay.classList.remove('hidden');
document.body.style.overflow = 'hidden'; // Prevent body scroll when sidebar is open
};
const closeMobileSidebar = () => {
dom.sidebar.classList.add('-translate-x-full');
dom.sidebarOverlay.classList.add('hidden');
document.body.style.overflow = ''; // Restore body scroll
};
const toggleTheme = () => {
document.documentElement.classList.toggle('dark');
localStorage.theme = document.documentElement.classList.contains('dark') ? 'dark' : 'light';
};
const initializeTheme = () => {
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
};
const showToast = (message, type = 'error') => {
const colors = {
error: 'bg-red-500',
success: 'bg-green-500',
info: 'bg-blue-500'
};
const icon = {
error: 'fa-circle-xmark',
success: 'fa-circle-check',
info: 'fa-circle-info'
}
const toast = document.createElement('div');
toast.className = `toast flex items-center gap-2 sm:gap-3 ${colors[type]} text-white text-xs sm:text-sm font-semibold px-3 py-2 sm:px-4 sm:py-3 rounded-lg shadow-lg mb-2`;
toast.innerHTML = `<i class="fa-solid ${icon[type]} text-base sm:text-lg"></i> <p>${message}</p>`;
dom.toastContainer.appendChild(toast);
setTimeout(() => toast.remove(), 5000);
};
const addMessageToChat = (role, content, isHtml = false) => {
dom.welcomeScreen.classList.add('hidden');
dom.historyLoading.classList.add('hidden');
const messageContainer = document.createElement('div');
messageContainer.className = `flex items-start gap-2 sm:gap-3 w-full animate-fade-in-up ${role === 'user' ? 'user-message justify-end' : 'assistant-message'}`;
let messageHtml = '';
if (role === 'user') {
const escapedContent = escapeHtml(content);
messageHtml = `<div class="message-bubble"><p class="text-sm sm:text-base">${escapedContent}</p></div>`;
} else {
const avatar = `<div class="assistant-avatar w-8 h-8 sm:w-9 sm:h-9 rounded-full flex items-center justify-center flex-shrink-0 text-lg sm:text-xl text-white"></div>`;
// Ensure prose styles apply correctly to potentially complex HTML from assistant
const bubbleContent = isHtml ? `<div class="prose prose-sm sm:prose-base max-w-none dark:prose-invert">${content}</div>` : `<p class="text-sm sm:text-base">${escapeHtml(content)}</p>`;
messageHtml = `
${avatar}
<div class="message-bubble">
${bubbleContent}
</div>
`;
}
messageContainer.innerHTML = messageHtml;
dom.chatMessages.insertBefore(messageContainer, dom.loadingIndicator);
scrollToBottom();
};
const escapeHtml = (unsafe) => unsafe.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
const resetChat = () => {
dom.welcomeScreen.classList.remove('hidden');
// Remove all previous messages except the templates
const messages = dom.chatMessages.querySelectorAll('.user-message, .assistant-message:not(#loading-indicator)');
messages.forEach(msg => {
if (msg.id !== 'loading-indicator') { // Ensure we don't remove the template itself
msg.remove();
}
});
dom.promptInput.value = '';
clearFileInput();
adjustTextareaHeight();
dom.promptInput.focus();
};
// --- File Handling ---
const clearFileInput = () => {
currentFile = null;
dom.fileUpload.value = ''; // Reset file input
dom.previewArea.innerHTML = '';
dom.previewArea.classList.add('hidden');
updateSendButtonState();
};
const handleFileChange = () => {
if (dom.fileUpload.files.length > 0) {
currentFile = dom.fileUpload.files[0];
// Validate file size (e.g., 5MB limit)
const maxSize = 5 * 1024 * 1024; // 5MB
if (currentFile.size > maxSize) {
showToast('Le fichier est trop volumineux (max 5MB).', 'error');
clearFileInput();
return;
}
dom.previewArea.classList.remove('hidden');
const fileChip = document.createElement('div');
fileChip.className = "inline-flex items-center bg-primary-100 dark:bg-primary-900/50 text-primary-700 dark:text-primary-200 text-xs sm:text-sm font-medium pl-2 pr-1 py-1 sm:pl-3 sm:pr-2 sm:py-1 rounded-full";
fileChip.innerHTML = `
<i class="fa-solid fa-file mr-1.5 sm:mr-2"></i>
<span class="max-w-[150px] sm:max-w-[200px] truncate" title="${escapeHtml(currentFile.name)}">${escapeHtml(currentFile.name)}</span>
<button type="button" class="ml-1.5 sm:ml-2 text-primary-500 hover:text-primary-700 dark:text-primary-300 dark:hover:text-primary-100 p-0.5 rounded-full hover:bg-primary-200 dark:hover:bg-primary-700/50">
<i class="fa-solid fa-xmark text-xs sm:text-sm"></i>
</button>
`;
dom.previewArea.innerHTML = ''; // Clear previous preview
dom.previewArea.appendChild(fileChip);
fileChip.querySelector('button').addEventListener('click', clearFileInput);
updateSendButtonState();
}
};
// --- API Calls ---
const loadChatHistory = async () => {
dom.historyLoading.classList.remove('hidden');
dom.welcomeScreen.classList.add('hidden');
try {
const response = await fetch(API_HISTORY_ENDPOINT);
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const data = await response.json();
if (!data.success) throw new Error(data.error || 'Failed to load history');
if (data.history.length > 0) {
dom.welcomeScreen.classList.add('hidden');
// Clear any existing messages before loading history to prevent duplicates
const messages = dom.chatMessages.querySelectorAll('.user-message, .assistant-message:not(#loading-indicator)');
messages.forEach(msg => msg.id !== 'loading-indicator' && msg.remove());
data.history.forEach(msg => addMessageToChat(msg.role, msg.text, msg.role === 'assistant'));
scrollToBottom('auto');
} else {
dom.welcomeScreen.classList.remove('hidden');
}
} catch (error) {
console.error("Error loading chat history:", error);
showToast(`Erreur de chargement de l'historique: ${error.message}`, 'error');
dom.welcomeScreen.classList.remove('hidden'); // Show welcome if history fails
} finally {
dom.historyLoading.classList.add('hidden');
}
};
const clearChatHistory = async () => {
try {
const response = await fetch(CLEAR_ENDPOINT, { method: 'POST' });
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const data = await response.json();
if (!data.success) throw new Error(data.error || 'Failed to clear chat');
resetChat();
showToast('Conversation effacée.', 'success');
if (window.innerWidth < 768) { // md breakpoint
closeMobileSidebar();
}
} catch(error) {
console.error("Error clearing chat history:", error);
showToast(`Erreur lors de l'effacement: ${error.message}`, 'error');
}
};
// --- Event Listeners ---
dom.themeToggle.addEventListener('click', toggleTheme);
// Sidebar Mobile Listeners
dom.sidebarToggle.addEventListener('click', openMobileSidebar);
dom.closeSidebarBtn.addEventListener('click', closeMobileSidebar);
dom.sidebarOverlay.addEventListener('click', closeMobileSidebar);
dom.newChatBtn.addEventListener('click', clearChatHistory);
dom.promptInput.addEventListener('input', adjustTextareaHeight);
dom.promptInput.addEventListener('keydown', (e) => {
if (isComposing) return;
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
if (!dom.sendButton.disabled) {
dom.chatForm.requestSubmit();
}
}
});
dom.promptInput.addEventListener('compositionstart', () => { isComposing = true; });
dom.promptInput.addEventListener('compositionend', () => { isComposing = false; adjustTextareaHeight(); });
dom.fileUpload.addEventListener('change', handleFileChange);
dom.suggestionCards.forEach(card => {
card.addEventListener('click', () => {
dom.promptInput.value = card.dataset.prompt;
dom.promptInput.focus();
adjustTextareaHeight(); // Ensure height is correct
updateSendButtonState(); // Ensure button state is correct
// Small delay to allow UI update before submitting
setTimeout(() => dom.chatForm.requestSubmit(), 50);
});
});
dom.chatForm.addEventListener('submit', async (e) => {
e.preventDefault();
const prompt = dom.promptInput.value.trim();
if (!prompt && !currentFile) return;
let userMessageContent = prompt;
// Construct a message that indicates a file is attached, if any
if (currentFile) {
userMessageContent = prompt ? `${prompt}\n\n[Fichier attaché: ${currentFile.name}]` : `[Fichier attaché: ${currentFile.name}]`;
}
addMessageToChat('user', userMessageContent);
const formData = new FormData();
formData.append('prompt', prompt); // Send original prompt text
formData.append('web_search', dom.webSearchToggle.checked);
if (currentFile) {
formData.append('file', currentFile, currentFile.name);
}
dom.promptInput.value = '';
clearFileInput(); // This also calls updateSendButtonState
adjustTextareaHeight();
setUIState(true);
try {
const response = await fetch(API_CHAT_ENDPOINT, { method: 'POST', body: formData });
if (!response.ok) {
let errorData;
try {
errorData = await response.json();
} catch (jsonError) {
// If response is not JSON, use status text
throw new Error(response.statusText || `Erreur serveur: ${response.status}`);
}
throw new Error(errorData.error || `Erreur serveur: ${response.status}`);
}
const data = await response.json();
addMessageToChat('assistant', data.message, true); // Assume assistant message can be HTML
} catch (error) {
console.error("Error submitting chat:", error);
const errorMessage = `<p class="text-red-600 dark:text-red-400">Désolé, une erreur est survenue :<br>${escapeHtml(error.message)}</p>`;
addMessageToChat('assistant', errorMessage, true);
} finally {
setUIState(false);
dom.promptInput.focus();
}
});
// --- Initialization ---
initializeTheme();
loadChatHistory();
adjustTextareaHeight(); // Initial height adjustment
updateSendButtonState(); // Initial button state
});
</script>
</body>
</html>