Download templates/index.html from Docfile/Chatm2: direct link, hf CLI and curl.
- Browser
- Download file 20.9 kB
-
https://huggingface.co/spaces/Docfile/Chatm2/resolve/main/templates/index.html
- Command line
-
hf download hf://spaces/Docfile/Chatm2/templates/index.html
-
curl -L -o index.html https://huggingface.co/spaces/Docfile/Chatm2/resolve/main/templates/index.html
20.9 kB
| <html lang="fr"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>AI Assistant</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/4.3.0/marked.min.js"></script> | |
| <script> | |
| tailwind.config = { | |
| darkMode: 'class', | |
| theme: { | |
| extend: { | |
| colors: { | |
| 'dark-bg': '#121212', | |
| 'dark-card': '#1e1e1e', | |
| 'dark-elevated': '#2c2c2c', | |
| 'dark-border': '#333', | |
| 'dark-text': '#f1f1f1', | |
| 'dark-muted': '#aaa' | |
| } | |
| } | |
| } | |
| } | |
| </script> | |
| <style> | |
| /* Configuration Marked pour le markdown */ | |
| .markdown-content { @apply text-dark-text leading-relaxed; } | |
| .markdown-content h1, .markdown-content h2, .markdown-content h3 { @apply font-bold mb-2 mt-4; } | |
| .markdown-content h1 { @apply text-xl; } | |
| .markdown-content h2 { @apply text-lg; } | |
| .markdown-content h3 { @apply text-base; } | |
| .markdown-content p { @apply mb-2; } | |
| .markdown-content ul, .markdown-content ol { @apply ml-4 mb-2 list-disc; } | |
| .markdown-content li { @apply mb-1; } | |
| .markdown-content code { @apply bg-gray-700 px-2 py-1 rounded text-sm font-mono; } | |
| .markdown-content pre { @apply bg-gray-800 p-4 rounded-lg overflow-x-auto mb-4; } | |
| .markdown-content pre code { @apply bg-transparent p-0; } | |
| .markdown-content blockquote { @apply border-l-4 border-gray-600 pl-4 italic text-gray-300 mb-4; } | |
| .markdown-content strong { @apply font-semibold; } | |
| .markdown-content em { @apply italic; } | |
| .markdown-content a { @apply text-blue-400 hover:text-blue-300 underline; } | |
| /* Animations et styles personnalisés */ | |
| @keyframes typing { 0%, 60%, 100% { transform: scale(0.8); opacity: 0.5; } 30% { transform: scale(1.2); opacity: 1; } } | |
| @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } | |
| .animate-typing { animation: typing 1.4s infinite ease-in-out; } | |
| .animate-fade-in { animation: fadeIn 0.3s ease; } | |
| /* Scrollbar personnalisée */ | |
| .custom-scrollbar::-webkit-scrollbar { width: 8px; } | |
| .custom-scrollbar::-webkit-scrollbar-track { background: #1e1e1e; } | |
| .custom-scrollbar::-webkit-scrollbar-thumb { background: #555; border-radius: 4px; } | |
| .custom-scrollbar::-webkit-scrollbar-thumb:hover { background: #777; } | |
| /* Style pour les pensées déroulantes */ | |
| .thoughts-toggle:checked + .thoughts-content { max-height: 1000px; opacity: 1; } | |
| .thoughts-content { max-height: 0; opacity: 0; overflow: hidden; transition: max-height 0.3s ease, opacity 0.3s ease; } | |
| </style> | |
| </head> | |
| <body class="bg-dark-bg text-dark-text min-h-screen flex justify-center items-center p-0 md:p-5"> | |
| <div class="w-full max-w-4xl h-screen md:h-[90vh] bg-dark-card flex flex-col overflow-hidden md:rounded-xl md:border md:border-dark-border"> | |
| <!-- Header simplifié pour un design plus aéré --> | |
| <header class="flex justify-between items-center p-4 md:px-6 border-b border-dark-border flex-shrink-0"> | |
| <h1 class="text-xl font-semibold">Mariam AI Assistant</h1> | |
| <button class="bg-transparent border border-gray-600 text-gray-300 px-4 py-2 rounded-full text-sm hover:bg-gray-700 transition-colors" onclick="resetConversation()"> | |
| 🔄 Reset | |
| </button> | |
| </header> | |
| <!-- Messages --> | |
| <div class="flex-1 overflow-y-auto p-4 md:p-6 flex flex-col gap-5 custom-scrollbar" id="messages"> | |
| <div class="max-w-[85%] p-4 rounded-2xl bg-dark-elevated text-gray-200 self-start rounded-bl-sm animate-fade-in"> | |
| <div class="markdown-content">👋 Bonjour, sur quoi allons-nous travailler aujourd'hui ?</div> | |
| </div> | |
| </div> | |
| <!-- Typing Indicator --> | |
| <div class="px-4 md:px-6 pb-2 hidden items-center gap-2 text-gray-500 text-sm italic" id="typingIndicator"> | |
| <span>L'assistant réfléchit</span> | |
| <div class="flex gap-1"> | |
| <span class="w-2 h-2 rounded-full bg-gray-600 animate-typing"></span> | |
| <span class="w-2 h-2 rounded-full bg-gray-600 animate-typing" style="animation-delay: 0.2s;"></span> | |
| <span class="w-2 h-2 rounded-full bg-gray-600 animate-typing" style="animation-delay: 0.4s;"></span> | |
| </div> | |
| </div> | |
| <!-- Input Container --> | |
| <div class="p-4 md:p-6 bg-dark-card border-t border-dark-border flex-shrink-0"> | |
| <div class="mb-3 p-3 bg-dark-elevated rounded-lg text-sm hidden" id="filePreview"></div> | |
| <div class="flex gap-3 items-end"> | |
| <input type="file" id="fileInput" class="hidden" accept="image/*,video/*,.pdf,.txt,.csv,.json" multiple onchange="handleFileSelect(event)"> | |
| <button class="bg-gray-700 text-white h-12 w-12 rounded-full hover:bg-gray-600 transition-colors flex-shrink-0 flex items-center justify-center text-xl" onclick="document.getElementById('fileInput').click()" title="Joindre un fichier"> | |
| 📎 | |
| </button> | |
| <div class="flex-1 relative"> | |
| <textarea | |
| id="messageInput" | |
| class="w-full min-h-[48px] p-3 pr-14 border border-gray-600 rounded-3xl text-base resize-none outline-none transition-colors bg-dark-elevated text-dark-text placeholder-gray-500 focus:border-gray-500" | |
| placeholder="Tapez votre message ici..." | |
| rows="1" | |
| onkeydown="handleKeyDown(event)" | |
| oninput="autoResize(this)" | |
| ></textarea> | |
| <button class="absolute right-1 top-1/2 transform -translate-y-1/2 bg-gray-700 hover:bg-gray-600 disabled:bg-gray-800 disabled:text-gray-600 border-none rounded-full w-10 h-10 text-white cursor-pointer transition-colors flex items-center justify-center text-lg" id="sendButton" onclick="sendMessage()" title="Envoyer"> | |
| ➤ | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| let currentFiles = []; | |
| let conversationId = 'session_' + Date.now(); | |
| marked.setOptions({ | |
| breaks: true, | |
| gfm: true | |
| }); | |
| function autoResize(textarea) { | |
| textarea.style.height = 'auto'; | |
| textarea.style.height = Math.min(textarea.scrollHeight, 150) + 'px'; | |
| } | |
| function handleKeyDown(event) { | |
| if (event.key === 'Enter' && !event.shiftKey) { | |
| event.preventDefault(); | |
| sendMessage(); | |
| } | |
| } | |
| function handleFileSelect(event) { | |
| const files = Array.from(event.target.files); | |
| if (files.length === 0) return; | |
| const filePreview = document.getElementById('filePreview'); | |
| currentFiles = []; | |
| let previews = []; | |
| files.forEach((file, index) => { | |
| const formData = new FormData(); | |
| formData.append('file', file); | |
| if (file.type.startsWith('image/')) { | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| previews[index] = ` | |
| <div class="flex items-center gap-3"> | |
| <img src="${e.target.result}" class="w-16 h-16 object-cover rounded-lg border border-gray-600"> | |
| <div class="text-gray-500 text-xs">${(file.size / 1024).toFixed(1)} KB</div> | |
| </div> | |
| `; | |
| updatePreview(); | |
| }; | |
| reader.readAsDataURL(file); | |
| } else { | |
| previews[index] = ` | |
| <div class="flex items-center gap-3"> | |
| <span class="text-gray-300">📎</span> | |
| <div class="text-gray-500 text-xs">${(file.size / 1024).toFixed(1)} KB</div> | |
| </div> | |
| `; | |
| updatePreview(); | |
| } | |
| fetch('/upload', { | |
| method: 'POST', | |
| body: formData | |
| }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.success) { | |
| currentFiles.push(data); | |
| } else { | |
| showError('Erreur lors du téléchargement: ' + data.error); | |
| } | |
| }) | |
| .catch(error => { | |
| showError('Erreur lors du téléchargement: ' + error.message); | |
| }); | |
| }); | |
| function updatePreview() { | |
| if (previews.filter(p => p).length === files.length) { | |
| filePreview.innerHTML = ` | |
| <div class="flex flex-wrap gap-3 items-center"> | |
| ${previews.join('')} | |
| <button onclick="removeFile()" class="bg-gray-600 hover:bg-gray-500 text-white px-3 py-1 rounded transition-colors">✕</button> | |
| </div> | |
| `; | |
| filePreview.classList.remove('hidden'); | |
| } | |
| } | |
| } | |
| function removeFile() { | |
| currentFiles = []; | |
| document.getElementById('filePreview').classList.add('hidden'); | |
| document.getElementById('fileInput').value = ''; | |
| } | |
| function sendMessage() { | |
| const messageInput = document.getElementById('messageInput'); | |
| const message = messageInput.value.trim(); | |
| if (!message && currentFiles.length === 0) return; | |
| const sendButton = document.getElementById('sendButton'); | |
| const typingIndicator = document.getElementById('typingIndicator'); | |
| messageInput.disabled = true; | |
| sendButton.disabled = true; | |
| if (message || currentFiles.length > 0) { | |
| addMessage('user', message, currentFiles); | |
| } | |
| typingIndicator.classList.remove('hidden'); | |
| typingIndicator.classList.add('flex'); | |
| messageInput.value = ''; | |
| autoResize(messageInput); | |
| // Le toggle a été retiré, on active la réflexion par défaut. | |
| const thinkingEnabled = true; | |
| const endpoint = currentFiles.length > 0 ? '/chat_with_file' : '/chat'; | |
| const payload = { | |
| message: message || 'Analyse ces fichiers', | |
| thinking_enabled: thinkingEnabled, | |
| conversation_id: conversationId | |
| }; | |
| if (currentFiles.length > 0) { | |
| payload.file_data = currentFiles; | |
| } | |
| fetch(endpoint, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(payload) | |
| }) | |
| .then(response => { | |
| if (!response.ok) { | |
| throw new Error(`Erreur réseau: ${response.status} ${response.statusText}`); | |
| } | |
| const reader = response.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| let buffer = ''; | |
| let messageElement = null; | |
| let thoughtsElement = null; | |
| let thoughtsContainer = null; | |
| let messageContent = ''; | |
| let thoughtsContent = ''; | |
| function processStream() { | |
| return reader.read().then(({ done, value }) => { | |
| if (done) { return; } | |
| buffer += decoder.decode(value, { stream: true }); | |
| const lines = buffer.split('\n'); | |
| buffer = lines.pop() || ''; | |
| for (const line of lines) { | |
| if (line.startsWith('data: ')) { | |
| try { | |
| const data = JSON.parse(line.substring(6)); | |
| if (data.type === 'text') { | |
| messageContent += data.content; | |
| if (!messageElement) { | |
| messageElement = addMessage('assistant', ''); | |
| } | |
| const markdownHtml = marked.parse(messageContent); | |
| const markdownElement = messageElement.querySelector('.markdown-content'); | |
| if (markdownElement) { | |
| markdownElement.innerHTML = markdownHtml; | |
| markdownElement.style.display = 'block'; | |
| } | |
| } else if (data.type === 'thought' && thinkingEnabled) { | |
| thoughtsContent += data.content; | |
| if (!thoughtsContainer) { | |
| thoughtsContainer = addThoughtsContainer(); | |
| } | |
| const thoughtsHtml = marked.parse(thoughtsContent); | |
| thoughtsContainer.querySelector('.thoughts-content .markdown-content').innerHTML = thoughtsHtml; | |
| } else if (data.type === 'error') { | |
| showError(data.content); | |
| } else if (data.type === 'end') { | |
| typingIndicator.classList.add('hidden'); | |
| typingIndicator.classList.remove('flex'); | |
| messageInput.disabled = false; | |
| sendButton.disabled = false; | |
| messageInput.focus(); | |
| removeFile(); | |
| return; | |
| } | |
| const messagesContainer = document.getElementById('messages'); | |
| messagesContainer.scrollTop = messagesContainer.scrollHeight; | |
| } catch (e) { | |
| console.error('Erreur parsing JSON:', e, "Ligne reçue:", line); | |
| } | |
| } | |
| } | |
| return processStream(); | |
| }); | |
| } | |
| return processStream(); | |
| }) | |
| .catch(error => { | |
| typingIndicator.classList.add('hidden'); | |
| typingIndicator.classList.remove('flex'); | |
| messageInput.disabled = false; | |
| sendButton.disabled = false; | |
| showError('Erreur: ' + error.message); | |
| messageInput.focus(); | |
| }); | |
| } | |
| function addMessage(role, content, fileData = null) { | |
| const messagesContainer = document.getElementById('messages'); | |
| const messageDiv = document.createElement('div'); | |
| let messageContent = ''; | |
| if (fileData) { | |
| const files = Array.isArray(fileData) ? fileData : [fileData]; | |
| files.forEach(file => { | |
| if (file.mime_type && file.mime_type.startsWith('image/')) { | |
| const imageUrl = `data:${file.mime_type};base64,${file.data}`; | |
| messageContent += `<img src="${imageUrl}" class="max-w-full h-auto rounded-lg mb-2 border border-gray-600">`; | |
| } | |
| }); | |
| } | |
| const markdownHtml = marked.parse(content || ''); | |
| messageContent += `<div class="markdown-content">${markdownHtml}</div>`; | |
| messageDiv.className = role === 'user' | |
| ? 'max-w-[85%] p-4 rounded-2xl bg-gray-600 text-gray-100 self-end rounded-br-sm animate-fade-in' | |
| : 'max-w-[85%] p-4 rounded-2xl bg-dark-elevated text-gray-200 self-start rounded-bl-sm animate-fade-in'; | |
| messageDiv.innerHTML = messageContent; | |
| messagesContainer.appendChild(messageDiv); | |
| messagesContainer.scrollTop = messagesContainer.scrollHeight; | |
| return messageDiv; | |
| } | |
| function addThoughtsContainer() { | |
| const messagesContainer = document.getElementById('messages'); | |
| const thoughtsDiv = document.createElement('div'); | |
| thoughtsDiv.className = 'bg-gray-800 border-l-4 border-gray-600 rounded-lg p-4 mb-2 max-w-[90%] self-start'; | |
| thoughtsDiv.innerHTML = ` | |
| <div class="flex items-center justify-between cursor-pointer" onclick="toggleThoughts(this)"> | |
| <div class="flex items-center gap-2 font-medium text-gray-300"> | |
| <span>🧠</span> | |
| <span>Réflexion de l'assistant</span> | |
| </div> | |
| <svg class="w-5 h-5 text-gray-400 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path> | |
| </svg> | |
| </div> | |
| <input type="checkbox" class="thoughts-toggle hidden"> | |
| <div class="thoughts-content mt-3 text-sm text-gray-400 italic"> | |
| <div class="markdown-content"></div> | |
| </div> | |
| `; | |
| messagesContainer.appendChild(thoughtsDiv); | |
| messagesContainer.scrollTop = messagesContainer.scrollHeight; | |
| return thoughtsDiv; | |
| } | |
| function toggleThoughts(element) { | |
| const container = element.parentElement; | |
| const checkbox = container.querySelector('.thoughts-toggle'); | |
| const arrow = element.querySelector('svg'); | |
| checkbox.checked = !checkbox.checked; | |
| arrow.style.transform = checkbox.checked ? 'rotate(180deg)' : 'rotate(0deg)'; | |
| } | |
| function showError(message) { | |
| const messagesContainer = document.getElementById('messages'); | |
| const errorDiv = document.createElement('div'); | |
| errorDiv.className = 'bg-red-900 text-red-200 border-l-4 border-red-600 p-4 rounded-lg mb-2'; | |
| errorDiv.textContent = message; | |
| messagesContainer.appendChild(errorDiv); | |
| messagesContainer.scrollTop = messagesContainer.scrollHeight; | |
| } | |
| function resetConversation() { | |
| if (confirm('Êtes-vous sûr de vouloir réinitialiser la conversation ?')) { | |
| fetch('/reset_conversation', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ conversation_id: conversationId }) | |
| }) | |
| .then(response => { | |
| if (response.ok) { | |
| document.getElementById('messages').innerHTML = ` | |
| <div class="max-w-[85%] p-4 rounded-2xl bg-dark-elevated text-gray-200 self-start rounded-bl-sm animate-fade-in"> | |
| <div class="markdown-content">👋 Conversation réinitialisée ! Comment puis-je vous aider ?</div> | |
| </div> | |
| `; | |
| conversationId = 'session_' + Date.now(); | |
| removeFile(); | |
| } else { | |
| showError('La réinitialisation a échoué.'); | |
| } | |
| }) | |
| .catch(error => { | |
| showError('Erreur lors de la réinitialisation: ' + error.message); | |
| }); | |
| } | |
| } | |
| document.addEventListener('DOMContentLoaded', function() { | |
| document.getElementById('messageInput').focus(); | |
| }); | |
| </script> | |
| </body> | |
| </html> |