|
<!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> |
|
|
|
<script src="https://cdn.tailwindcss.com?plugins=forms,typography"></script> |
|
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"> |
|
|
|
<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"> |
|
|
|
<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; |
|
} |
|
body { |
|
font-family: 'Inter', sans-serif; |
|
background: #f8fafc; |
|
color: #0f172a; |
|
} |
|
.dark body { |
|
background: #0f172a; |
|
color: #f1f5f9; |
|
} |
|
::-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; } |
|
|
|
|
|
.main-layout { display: flex; height: 100vh; } |
|
|
|
.chat-container { display: flex; flex-direction: column; height: 100%; } |
|
|
|
|
|
.suggestion-card { |
|
transition: all 0.2s ease-in-out; |
|
border: 1px solid #e2e8f0; |
|
} |
|
.dark .suggestion-card { border-color: #334155; } |
|
.suggestion-card:hover { |
|
transform: translateY(-4px); |
|
box-shadow: 0 4px 10px rgba(0,0,0,0.05); |
|
border-color: #93c5fd; |
|
} |
|
.dark .suggestion-card:hover { |
|
box-shadow: 0 4px 20px rgba(0,0,0,0.2); |
|
border-color: #3b82f6; |
|
} |
|
|
|
|
|
.message-bubble { |
|
max-width: 80%; |
|
padding: 0.75rem 1.125rem; |
|
border-radius: 1.25rem; |
|
animation: fadeInUp 0.4s ease-out; |
|
} |
|
.user-message .message-bubble { |
|
background-color: #2563eb; |
|
color: white; |
|
border-bottom-right-radius: 0.25rem; |
|
} |
|
.assistant-message .message-bubble { |
|
background-color: #ffffff; |
|
color: #1e293b; |
|
border: 1px solid #e2e8f0; |
|
border-bottom-left-radius: 0.25rem; |
|
} |
|
.dark .assistant-message .message-bubble { |
|
background-color: #1e293b; |
|
color: #e2e8f0; |
|
border-color: #334155; |
|
} |
|
.assistant-avatar { |
|
background: linear-gradient(135deg, #3b82f6, #9333ea); |
|
} |
|
|
|
|
|
.chat-input-area { |
|
resize: none; |
|
min-height: 52px; |
|
max-height: 200px; |
|
} |
|
.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"> |
|
|
|
<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"> |
|
|
|
</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> |
|
|
|
<div id="sidebar-overlay" class="fixed inset-0 z-30 bg-black/30 backdrop-blur-sm hidden md:hidden"></div> |
|
|
|
|
|
<main class="flex-1 chat-container"> |
|
|
|
<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> |
|
|
|
|
|
<div id="chat-messages" class="flex-1 overflow-y-auto p-4 sm:p-6 space-y-4 sm:space-y-6"> |
|
|
|
<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> |
|
|
|
|
|
<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> |
|
|
|
|
|
<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> |
|
|
|
|
|
<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> |