Chatm2 / templates /index.html
Docfile's picture
Update templates/index.html
0a3c1c2 verified
raw
history blame
20.3 kB
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Assistant - Flat Black & White</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css"></script>
<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;
}
.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 -->
<div class="bg-dark-card text-white p-5 text-center border-b border-dark-border flex-shrink-0">
<h1 class="text-xl font-semibold mb-1">Mariam AI Assistant</h1>
<p class="text-sm text-dark-muted">❤️</p>
<div class="flex justify-center items-center gap-5 mt-4">
<div class="flex items-center gap-3 text-sm text-gray-300">
<span>Réflexion Mode:</span>
<label class="relative inline-block w-12 h-7">
<input type="checkbox" id="thinkingToggle" checked class="opacity-0 w-0 h-0">
<span class="absolute cursor-pointer top-0 left-0 right-0 bottom-0 bg-gray-600 transition-all duration-200 rounded-full before:absolute before:content-[''] before:h-5 before:w-5 before:left-1 before:bottom-1 before:bg-white before:transition-all before:duration-200 before:rounded-full checked:bg-gray-500 checked:before:translate-x-5"></span>
</label>
</div>
<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>
</div>
</div>
<!-- Messages -->
<div class="flex-1 overflow-y-auto p-4 md:p-6 flex flex-col gap-4 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" 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 currentFile = null;
let conversationId = 'session_' + Date.now();
// Configuration de marked pour le markdown
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 file = event.target.files[0];
if (!file) return;
const filePreview = document.getElementById('filePreview');
const formData = new FormData();
formData.append('file', file);
fetch('/upload', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
currentFile = data;
filePreview.innerHTML = `
<div class="flex justify-between items-center">
<span class="text-gray-300">📎 ${data.filename}</span>
<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');
} else {
showError('Erreur lors du téléchargement: ' + data.error);
}
})
.catch(error => {
showError('Erreur lors du téléchargement: ' + error.message);
});
}
function removeFile() {
currentFile = null;
document.getElementById('filePreview').classList.add('hidden');
document.getElementById('fileInput').value = '';
}
function sendMessage() {
const messageInput = document.getElementById('messageInput');
const message = messageInput.value.trim();
if (!message && !currentFile) return;
const sendButton = document.getElementById('sendButton');
const typingIndicator = document.getElementById('typingIndicator');
messageInput.disabled = true;
sendButton.disabled = true;
if (message) {
addMessage('user', message);
}
typingIndicator.classList.remove('hidden');
typingIndicator.classList.add('flex');
messageInput.value = '';
autoResize(messageInput);
const thinkingEnabled = document.getElementById('thinkingToggle').checked;
const endpoint = currentFile ? '/chat_with_file' : '/chat';
const payload = {
message: message || 'Analyse ce fichier',
thinking_enabled: thinkingEnabled,
conversation_id: conversationId
};
if (currentFile) {
payload.file_data = currentFile;
}
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', '');
}
// Rendu markdown en temps réel
const markdownHtml = marked.parse(messageContent);
messageElement.querySelector('.markdown-content').innerHTML = markdownHtml;
} else if (data.type === 'thought' && thinkingEnabled) {
thoughtsContent += data.content;
if (!thoughtsContainer) {
thoughtsContainer = addThoughtsContainer();
}
// Rendu markdown des pensées
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) {
const messagesContainer = document.getElementById('messages');
const messageDiv = document.createElement('div');
if (role === 'user') {
messageDiv.className = 'max-w-[85%] p-4 rounded-2xl bg-gray-600 text-gray-100 self-end rounded-br-sm animate-fade-in';
const markdownHtml = marked.parse(content);
messageDiv.innerHTML = `<div class="markdown-content">${markdownHtml}</div>`;
} else {
messageDiv.className = 'max-w-[85%] p-4 rounded-2xl bg-dark-elevated text-gray-200 self-start rounded-bl-sm animate-fade-in';
messageDiv.innerHTML = `<div class="markdown-content">${content}</div>`;
}
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;
if (checkbox.checked) {
arrow.style.transform = 'rotate(180deg)';
} else {
arrow.style.transform = '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>