|
|
<!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> |
|
|
|
|
|
.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; |
|
|
} |
|
|
|
|
|
|
|
|
@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; } |
|
|
|
|
|
|
|
|
.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; } |
|
|
|
|
|
|
|
|
.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"> |
|
|
|
|
|
<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> |
|
|
|
|
|
|
|
|
<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> |
|
|
|
|
|
|
|
|
<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> |
|
|
|
|
|
|
|
|
<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); |
|
|
|
|
|
const thinkingEnabled = document.getElementById('thinkingToggle').checked; |
|
|
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)); |
|
|
console.log('Received data:', data); |
|
|
|
|
|
if (data.type === 'text') { |
|
|
console.log('Processing text:', data.content); |
|
|
messageContent += data.content; |
|
|
if (!messageElement) { |
|
|
messageElement = addMessage('assistant', ''); |
|
|
console.log('Created message element:', messageElement); |
|
|
} |
|
|
|
|
|
const markdownHtml = marked.parse(messageContent); |
|
|
const markdownElement = messageElement.querySelector('.markdown-content'); |
|
|
if (markdownElement) { |
|
|
markdownElement.innerHTML = markdownHtml; |
|
|
console.log('Updated markdown content to:', markdownHtml); |
|
|
|
|
|
markdownElement.style.display = 'block'; |
|
|
} else { |
|
|
console.error('No .markdown-content element found'); |
|
|
console.log('Message element HTML:', messageElement.innerHTML); |
|
|
} |
|
|
} else if (data.type === 'thought' && thinkingEnabled) { |
|
|
console.log('Processing thought:', data.content); |
|
|
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') { |
|
|
console.log('Received error:', data.content); |
|
|
showError(data.content); |
|
|
} else if (data.type === 'end') { |
|
|
console.log('Stream ended'); |
|
|
typingIndicator.classList.add('hidden'); |
|
|
typingIndicator.classList.remove('flex'); |
|
|
messageInput.disabled = false; |
|
|
sendButton.disabled = false; |
|
|
messageInput.focus(); |
|
|
removeFile(); |
|
|
return; |
|
|
} else { |
|
|
console.log('Unknown data type:', data.type); |
|
|
} |
|
|
|
|
|
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) { |
|
|
console.log('addMessage called with role:', role, 'content:', content); |
|
|
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>`; |
|
|
console.log('Message content HTML:', messageContent); |
|
|
|
|
|
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'; |
|
|
} 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 = messageContent; |
|
|
console.log('Message div created:', messageDiv); |
|
|
|
|
|
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> |