Spaces:
Sleeping
Sleeping
<html lang="fr"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Mariam M-0</title> | |
<!-- Intégration de Tailwind CSS --> | |
<script defer src="https://cdn.tailwindcss.com"></script> | |
<!-- Marked pour le rendu Markdown --> | |
<script defer src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> | |
<!-- DOMPurify pour sécuriser le rendu HTML --> | |
<script defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/purify.min.js"></script> | |
<!-- Highlight.js pour la coloration syntaxique --> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css"> | |
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script> | |
<style> | |
@keyframes gradient { | |
0% { background-position: 0% 50%; } | |
50% { background-position: 100% 50%; } | |
100% { background-position: 0% 50%; } | |
} | |
.gradient-bg { | |
background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab); | |
background-size: 400% 400%; | |
animation: gradient 15s ease infinite; | |
} | |
/* Assurer la bonne gestion des espaces et retours à la ligne sur mobile */ | |
.markdown-content { | |
white-space: pre-wrap; | |
word-break: break-word; | |
} | |
/* Styles pour le rendu Markdown en CSS natif */ | |
.markdown-content h1 { | |
font-size: 1.5rem; /* text-2xl */ | |
font-weight: 700; /* font-bold */ | |
margin-top: 1.5rem; /* mt-6 */ | |
margin-bottom: 1rem; /* mb-4 */ | |
} | |
.markdown-content h2 { | |
font-size: 1.25rem; /* text-xl */ | |
font-weight: 700; | |
margin-top: 1.25rem; /* mt-5 */ | |
margin-bottom: 0.75rem; /* mb-3 */ | |
} | |
.markdown-content h3 { | |
font-size: 1.125rem; /* text-lg */ | |
font-weight: 700; | |
margin-top: 1rem; /* mt-4 */ | |
margin-bottom: 0.5rem; /* mb-2 */ | |
} | |
.markdown-content p { | |
margin-bottom: 1rem; /* mb-4 */ | |
line-height: 1.625; /* leading-relaxed */ | |
} | |
.markdown-content ul { | |
list-style-type: disc; | |
margin-left: 1.5rem; /* ml-6 */ | |
margin-bottom: 1rem; /* mb-4 */ | |
} | |
.markdown-content ol { | |
list-style-type: decimal; | |
margin-left: 1.5rem; | |
margin-bottom: 1rem; | |
} | |
.markdown-content li { | |
margin-bottom: 0.25rem; /* mb-1 */ | |
} | |
.markdown-content a { | |
color: #2563eb; /* text-blue-600 */ | |
text-decoration: underline; | |
} | |
.markdown-content a:hover { | |
color: #1d4ed8; /* text-blue-800 */ | |
} | |
.markdown-content blockquote { | |
padding-left: 1rem; /* pl-4 */ | |
border-left: 4px solid #D1D5DB; /* border-gray-300 */ | |
font-style: italic; | |
margin: 1rem 0; /* my-4 */ | |
} | |
.markdown-content code:not(pre code) { | |
background-color: #F3F4F6; /* bg-gray-100 */ | |
padding: 0 0.25rem; /* px-1 */ | |
border-radius: 0.25rem; /* rounded */ | |
font-size: 0.875rem; /* text-sm */ | |
font-family: monospace; | |
} | |
.markdown-content pre { | |
background-color: #F3F4F6; | |
padding: 1rem; /* p-4 */ | |
border-radius: 0.5rem; /* rounded-lg */ | |
margin-bottom: 1rem; /* mb-4 */ | |
overflow-x: auto; | |
} | |
.markdown-content table { | |
width: 100%; | |
border: 1px solid #D1D5DB; /* border-gray-300 */ | |
margin-bottom: 1rem; | |
border-collapse: collapse; | |
} | |
.markdown-content th, | |
.markdown-content td { | |
border-bottom: 1px solid #D1D5DB; | |
padding: 0.5rem 1rem; /* px-4 py-2 */ | |
text-align: left; | |
} | |
.markdown-content th { | |
background-color: #F3F4F6; | |
} | |
.markdown-content img { | |
max-width: 100%; | |
height: auto; | |
margin: 1rem 0; | |
border-radius: 0.5rem; | |
} | |
</style> | |
</head> | |
<body class="min-h-screen bg-gray-50"> | |
<!-- Barre de gradient fixe en haut de page --> | |
<div class="gradient-bg h-2 w-full fixed top-0"></div> | |
<div class="max-w-4xl mx-auto px-4 py-8"> | |
<header class="text-center mb-12"> | |
<h1 class="text-4xl font-bold text-gray-800 mb-2">Mariam M-0</h1> | |
<p class="text-gray-600">Votre assistant IA personnel</p> | |
</header> | |
<main> | |
<section class="bg-white rounded-xl shadow-lg p-6 mb-8"> | |
<textarea | |
id="question" | |
class="w-full h-32 p-4 border border-gray-200 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none" | |
placeholder="Posez votre question ici..." | |
aria-label="Zone de saisie de la question" | |
></textarea> | |
<button | |
id="submitBtn" | |
class="mt-4 px-6 py-3 bg-gradient-to-r from-blue-500 to-blue-600 text-white rounded-lg hover:from-blue-600 hover:to-blue-700 transition-all duration-200 flex items-center justify-center w-full sm:w-auto" | |
aria-label="Obtenir une réponse" | |
> | |
<span>Obtenir une réponse</span> | |
<div id="spinner" class="hidden ml-3 animate-spin rounded-full h-5 w-5 border-b-2 border-white" aria-hidden="true"></div> | |
</button> | |
</section> | |
<section id="responseContainer" class="space-y-6"> | |
<article id="answerBox" class="hidden bg-white rounded-xl shadow-lg p-6 relative"> | |
<div class="flex items-center justify-between mb-4"> | |
<h2 class="text-xl font-semibold text-gray-800">Réponse</h2> | |
<!-- Bouton Copier --> | |
<button | |
id="copyBtn" | |
class="px-3 py-1 bg-blue-500 text-white text-sm rounded hover:bg-blue-600 transition-colors" | |
title="Copier la réponse" | |
aria-label="Copier la réponse" | |
> | |
Copier | |
</button> | |
</div> | |
<div id="answer" class="markdown-content text-gray-700"></div> | |
</article> | |
<article id="thinkingBox" class="hidden bg-white rounded-xl shadow-lg p-6"> | |
<div class="flex items-center justify-between mb-4"> | |
<h2 class="text-xl font-semibold text-gray-800">Raisonnement</h2> | |
<button | |
id="toggleThinking" | |
class="text-blue-500 hover:text-blue-600 focus:outline-none" | |
aria-expanded="false" | |
aria-controls="thinking" | |
> | |
Afficher | |
</button> | |
</div> | |
<div id="thinking" class="hidden markdown-content text-gray-600"></div> | |
</article> | |
</section> | |
</main> | |
</div> | |
<script defer> | |
// Configuration de marked pour le rendu Markdown | |
marked.setOptions({ | |
highlight: function(code, lang) { | |
if (lang && hljs.getLanguage(lang)) { | |
return hljs.highlight(code, { language: lang }).value; | |
} | |
return hljs.highlightAuto(code).value; | |
}, | |
breaks: true, | |
gfm: true | |
}); | |
/** | |
* Fonction pour rendre le Markdown de manière sécurisée. | |
* @param {string} content - Le contenu en Markdown. | |
* @returns {string} Le HTML rendu et sécurisé. | |
*/ | |
function renderMarkdown(content) { | |
const rawHtml = marked.parse(content); | |
return DOMPurify.sanitize(rawHtml); | |
} | |
const submitBtn = document.getElementById('submitBtn'); | |
const spinner = document.getElementById('spinner'); | |
const answerBox = document.getElementById('answerBox'); | |
const thinkingBox = document.getElementById('thinkingBox'); | |
const answer = document.getElementById('answer'); | |
const thinking = document.getElementById('thinking'); | |
const toggleThinking = document.getElementById('toggleThinking'); | |
const copyBtn = document.getElementById('copyBtn'); | |
// Bouton pour basculer l'affichage du raisonnement | |
toggleThinking.addEventListener('click', () => { | |
const isHidden = thinking.classList.contains('hidden'); | |
thinking.classList.toggle('hidden'); | |
toggleThinking.textContent = isHidden ? 'Masquer' : 'Afficher'; | |
toggleThinking.setAttribute('aria-expanded', isHidden); | |
}); | |
// Bouton Copier | |
copyBtn.addEventListener('click', () => { | |
// Crée un élément temporaire pour copier le texte brut (sans HTML) | |
const tempElement = document.createElement('textarea'); | |
tempElement.value = answer.innerText; | |
document.body.appendChild(tempElement); | |
tempElement.select(); | |
try { | |
document.execCommand('copy'); | |
copyBtn.textContent = 'Copié'; | |
setTimeout(() => { | |
copyBtn.textContent = 'Copier'; | |
}, 1500); | |
} catch (err) { | |
console.error('Erreur lors de la copie :', err); | |
} | |
document.body.removeChild(tempElement); | |
}); | |
// Gestion de la soumission de la question | |
submitBtn.addEventListener('click', async () => { | |
const question = document.getElementById('question').value; | |
if (!question.trim()) return; | |
// Réinitialisation de l'interface utilisateur | |
answer.innerHTML = ''; | |
thinking.innerHTML = ''; | |
submitBtn.disabled = true; | |
spinner.classList.remove('hidden'); | |
answerBox.classList.add('hidden'); | |
thinkingBox.classList.add('hidden'); | |
try { | |
const response = await fetch('/ask', { | |
method: 'POST', | |
headers: { 'Content-Type': 'application/json' }, | |
body: JSON.stringify({ question }) | |
}); | |
// Lecture du flux de réponse en continu | |
const reader = response.body.getReader(); | |
const decoder = new TextDecoder(); | |
while (true) { | |
const { done, value } = await reader.read(); | |
if (done) break; | |
const chunks = decoder.decode(value).split('\n'); | |
chunks.forEach(chunk => { | |
if (!chunk) return; | |
const data = JSON.parse(chunk); | |
if (data.type === 'thinking') { | |
thinkingBox.classList.remove('hidden'); | |
thinking.innerHTML = renderMarkdown(data.content); | |
} else if (data.type === 'answer') { | |
answerBox.classList.remove('hidden'); | |
answer.innerHTML = renderMarkdown(data.content); | |
// Rafraîchissement de la coloration syntaxique pour les blocs de code | |
answer.querySelectorAll('pre code').forEach(block => { | |
hljs.highlightElement(block); | |
}); | |
} | |
}); | |
} | |
} catch (error) { | |
console.error('Error:', error); | |
answer.innerHTML = renderMarkdown("❌ Une erreur est survenue. Veuillez réessayer."); | |
answerBox.classList.remove('hidden'); | |
} finally { | |
submitBtn.disabled = false; | |
spinner.classList.add('hidden'); | |
} | |
}); | |
</script> | |
</body> | |
</html> | |