cygnis_ai / html /chat.html
Simonc-44's picture
Create chat.html
ed611a8 verified
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cygnis - Assistant IA Avancé</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: {
600: '#7c3aed',
700: '#6d28d9',
},
dark: {
800: '#1e293b',
900: '#0f172a',
}
}
}
}
}
</script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link rel="icon" href="https://svgsilh.com/svg/2034730.svg" type="image/svg+xml">
<style>
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.animate-pulse {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
.typing-indicator {
display: flex;
padding: 8px 12px;
}
.typing-dot {
width: 8px;
height: 8px;
margin: 0 2px;
background-color: #9ca3af;
border-radius: 50%;
animation: typing-dot 1.5s infinite ease-in-out;
}
.typing-dot:nth-child(1) { animation-delay: 0s; }
.typing-dot:nth-child(2) { animation-delay: 0.2s; }
.typing-dot:nth-child(3) { animation-delay: 0.4s; }
@keyframes typing-dot {
0%, 60%, 100% { transform: translateY(0); }
30% { transform: translateY(-5px); }
}
.message-transition {
transition: all 0.3s ease;
transform-origin: bottom;
}
.message-enter {
opacity: 0;
transform: scale(0.9);
}
.message-enter-active {
opacity: 1;
transform: scale(1);
}
.sidebar {
scrollbar-width: thin;
scrollbar-color: #4b5563 #1f2937;
}
.sidebar::-webkit-scrollbar {
width: 6px;
}
.sidebar::-webkit-scrollbar-track {
background: #1f2937;
}
.sidebar::-webkit-scrollbar-thumb {
background-color: #4b5563;
border-radius: 6px;
}
.chat-container {
scrollbar-width: thin;
scrollbar-color: #4b5563 #1f2937;
}
.chat-container::-webkit-scrollbar {
width: 6px;
}
.chat-container::-webkit-scrollbar-track {
background: #1f2937;
}
.chat-container::-webkit-scrollbar-thumb {
background-color: #4b5563;
border-radius: 6px;
}
.markdown-content h1, .markdown-content h2, .markdown-content h3 {
font-weight: bold;
margin-top: 1em;
margin-bottom: 0.5em;
}
.markdown-content h1 { font-size: 1.5em; }
.markdown-content h2 { font-size: 1.3em; }
.markdown-content h3 { font-size: 1.1em; }
.markdown-content p { margin-bottom: 1em; }
.markdown-content ul, .markdown-content ol {
margin-bottom: 1em;
padding-left: 2em;
}
.markdown-content ul { list-style-type: disc; }
.markdown-content ol { list-style-type: decimal; }
.markdown-content li { margin-bottom: 0.5em; }
.markdown-content code {
background-color: rgba(124, 58, 237, 0.2);
padding: 0.2em 0.4em;
border-radius: 0.25em;
font-family: monospace;
color: #e2e8f0;
}
.markdown-content pre {
background-color: rgba(15, 23, 42, 0.7);
padding: 1em;
border-radius: 0.5em;
overflow-x: auto;
margin-bottom: 1em;
border: 1px solid rgba(124, 58, 237, 0.3);
}
.markdown-content pre code {
background-color: transparent;
padding: 0;
}
.markdown-content a {
color: #818cf8;
text-decoration: underline;
}
.markdown-content blockquote {
border-left: 4px solid #4b5563;
padding-left: 1em;
margin-left: 0;
color: #9ca3af;
margin-bottom: 1em;
}
.markdown-content table {
width: 100%;
border-collapse: collapse;
margin-bottom: 1em;
}
.markdown-content th, .markdown-content td {
border: 1px solid #4b5563;
padding: 0.5em;
text-align: left;
}
.markdown-content th {
background-color: rgba(124, 58, 237, 0.2);
}
.gradient-bg {
background: linear-gradient(135deg, rgba(124, 58, 237, 0.1) 0%, rgba(15, 23, 42, 0.8) 100%);
}
.message-toolbar {
opacity: 0;
transition: opacity 0.2s ease;
}
.message-container:hover .message-toolbar {
opacity: 1;
}
.suggestion-chip {
transition: all 0.2s ease;
}
.suggestion-chip:hover {
transform: translateY(-2px);
background-color: rgba(124, 58, 237, 0.2);
}
.wave-animation {
display: inline-block;
position: relative;
}
.wave-animation span {
display: inline-block;
animation: wave 1.5s infinite;
animation-delay: calc(0.1s * var(--i));
}
@keyframes wave {
0%, 40%, 100% { transform: translateY(0); }
20% { transform: translateY(-5px); }
}
.blink {
animation: blink 1s step-end infinite;
}
@keyframes blink {
from, to { opacity: 1; }
50% { opacity: 0; }
}
</style>
</head>
<body class="bg-gray-900 text-gray-100 h-screen flex overflow-hidden">
<!-- Sidebar -->
<div class="w-64 bg-gray-800 border-r border-gray-700 flex flex-col h-full">
<!-- Sidebar Header -->
<div class="p-4 border-b border-gray-700">
<div class="flex items-center space-x-2">
<div class="w-8 h-8 rounded-full bg-primary-600 flex items-center justify-center">
<i class="fas fa-robot text-white"></i>
</div>
<h1 class="text-xl font-bold">Cygnis AI</h1>
</div>
<button id="new-chat-btn" class="mt-4 w-full bg-primary-600 hover:bg-primary-700 text-white py-2 px-4 rounded-lg flex items-center justify-center transition transform hover:scale-[1.02] active:scale-95">
<i class="fas fa-plus mr-2"></i> Nouvelle conversation
</button>
</div>
<!-- Search Conversations -->
<div class="p-2 border-b border-gray-700">
<div class="relative">
<input
id="search-conversations"
type="text"
placeholder="Rechercher des conversations..."
class="w-full bg-gray-700 text-white rounded-lg py-2 px-3 pl-8 text-sm focus:outline-none focus:ring-1 focus:ring-primary-600"
>
<i class="fas fa-search absolute left-3 top-2.5 text-gray-400 text-sm"></i>
</div>
</div>
<!-- Conversation List -->
<div class="flex-1 overflow-y-auto sidebar">
<div id="conversation-list" class="p-2 space-y-1"></div>
</div>
<!-- User Profile -->
<div class="p-4 border-t border-gray-700">
<div id="user-profile" class="flex items-center space-x-3 cursor-pointer group">
<div id="user-avatar" class="w-10 h-10 rounded-full bg-gray-700 flex items-center justify-center group-hover:bg-gray-600 transition">
<i class="fas fa-user"></i>
</div>
<div>
<div id="username" class="font-medium">Invité</div>
<div class="text-xs text-gray-400 flex items-center">
<span class="inline-block w-2 h-2 rounded-full bg-green-500 mr-1"></span>
Version Pro
</div>
</div>
</div>
</div>
</div>
<!-- Main Chat Area -->
<div class="flex-1 flex flex-col h-full">
<!-- Chat Header -->
<div class="bg-gray-800 p-4 border-b border-gray-700 flex items-center justify-between">
<div class="flex items-center space-x-2">
<h2 id="current-conversation-title" class="text-lg font-semibold">Nouvelle conversation</h2>
<button id="edit-title-btn" class="text-gray-400 hover:text-white">
<i class="fas fa-pencil-alt text-xs"></i>
</button>
</div>
<div class="flex items-center space-x-4">
<button id="suggestions-btn" class="text-gray-400 hover:text-white" title="Suggestions">
<i class="fas fa-lightbulb"></i>
</button>
<button id="settings-btn" class="text-gray-400 hover:text-white" title="Paramètres">
<i class="fas fa-cog"></i>
</button>
<button id="help-btn" class="text-gray-400 hover:text-white" title="Aide">
<i class="fas fa-question-circle"></i>
</button>
</div>
</div>
<!-- Suggestions Panel -->
<div id="suggestions-panel" class="bg-gray-800 border-b border-gray-700 p-3 hidden">
<div class="max-w-4xl mx-auto">
<h3 class="text-sm font-medium text-gray-400 mb-2">SUGGESTIONS</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-2">
<button class="suggestion-chip bg-gray-700 hover:bg-gray-600 text-sm py-2 px-3 rounded-lg text-left transition">
Explique-moi les concepts de l'apprentissage automatique comme si j'avais 5 ans
</button>
<button class="suggestion-chip bg-gray-700 hover:bg-gray-600 text-sm py-2 px-3 rounded-lg text-left transition">
Rédige un email professionnel pour reporter une réunion
</button>
<button class="suggestion-chip bg-gray-700 hover:bg-gray-600 text-sm py-2 px-3 rounded-lg text-left transition">
Génère un poème sur l'intelligence artificielle
</button>
<button class="suggestion-chip bg-gray-700 hover:bg-gray-600 text-sm py-2 px-3 rounded-lg text-left transition">
Donne-moi des idées de projets en Python pour débutants
</button>
<button class="suggestion-chip bg-gray-700 hover:bg-gray-600 text-sm py-2 px-3 rounded-lg text-left transition">
Quelle est la différence entre React et Vue.js ?
</button>
<button class="suggestion-chip bg-gray-700 hover:bg-gray-600 text-sm py-2 px-3 rounded-lg text-left transition">
Planifie un programme d'entraînement de 4 semaines pour débutant
</button>
</div>
</div>
</div>
<!-- Chat Messages -->
<div id="chat-messages" class="flex-1 overflow-y-auto chat-container p-4 space-y-4 gradient-bg">
<!-- Welcome Message -->
<div class="max-w-3xl mx-auto text-center py-10">
<div class="w-16 h-16 mx-auto rounded-full bg-primary-600 flex items-center justify-center mb-4">
<i class="fas fa-robot text-2xl text-white"></i>
</div>
<h2 class="text-2xl font-bold mb-2">
<span class="wave-animation">
<span style="--i:1">B</span>
<span style="--i:2">o</span>
<span style="--i:3">n</span>
<span style="--i:4">j</span>
<span style="--i:5">o</span>
<span style="--i:6">u</span>
<span style="--i:7">r</span>
<span style="--i:8">!</span>
</span>
</h2>
<p class="text-gray-400 mb-6">Je suis Cygnis, votre assistant IA basé sur Nous-Hermes-2-Mistral-7B-DPO. Comment puis-je vous aider aujourd'hui ?</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 max-w-xl mx-auto">
<button class="suggestion-chip bg-gray-800 hover:bg-gray-700 py-2 px-4 rounded-lg text-left transition">
<div class="font-medium">Expliquer un concept technique</div>
<div class="text-xs text-gray-400">"Qu'est-ce qu'un réseau de neurones ?"</div>
</button>
<button class="suggestion-chip bg-gray-800 hover:bg-gray-700 py-2 px-4 rounded-lg text-left transition">
<div class="font-medium">Générer du code</div>
<div class="text-xs text-gray-400">"Montre-moi un exemple de composant React"</div>
</button>
<button class="suggestion-chip bg-gray-800 hover:bg-gray-700 py-2 px-4 rounded-lg text-left transition">
<div class="font-medium">Rédiger du contenu</div>
<div class="text-xs text-gray-400">"Écris un article sur les bienfaits du yoga"</div>
</button>
<button class="suggestion-chip bg-gray-800 hover:bg-gray-700 py-2 px-4 rounded-lg text-left transition">
<div class="font-medium">Résoudre un problème</div>
<div class="text-xs text-gray-400">"Comment optimiser mes performances SQL ?"</div>
</button>
</div>
</div>
</div>
<!-- Input Area -->
<div class="bg-gray-800 p-4 border-t border-gray-700">
<div class="max-w-3xl mx-auto">
<div class="relative">
<textarea
id="message-input"
rows="1"
placeholder="Envoyez un message..."
class="w-full bg-gray-700 text-white rounded-lg py-3 px-4 pr-12 focus:outline-none focus:ring-2 focus:ring-primary-600 resize-none"
style="min-height: 44px; max-height: 200px;"
></textarea>
<div class="absolute right-3 bottom-3 flex space-x-2">
<button id="attach-btn" class="text-gray-400 hover:text-white">
<i class="fas fa-paperclip"></i>
</button>
<button id="send-btn" class="text-gray-400 hover:text-white">
<i class="fas fa-paper-plane"></i>
</button>
</div>
</div>
<div class="mt-2 text-xs text-gray-500 text-center">
Cygnis peut faire des erreurs. Vérifiez les informations importantes.
<span class="hidden md:inline-block ml-2">
<kbd class="bg-gray-700 px-1.5 py-0.5 rounded">Shift</kbd> + <kbd class="bg-gray-700 px-1.5 py-0.5 rounded">Enter</kbd> pour aller à la ligne
</span>
</div>
</div>
</div>
</div>
<!-- Settings Modal -->
<div id="settings-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
<div class="bg-gray-800 rounded-lg w-full max-w-md max-h-[90vh] overflow-y-auto">
<div class="p-4 border-b border-gray-700 flex justify-between items-center">
<h3 class="text-lg font-semibold">Paramètres</h3>
<button id="close-settings" class="text-gray-400 hover:text-white">
<i class="fas fa-times"></i>
</button>
</div>
<div class="p-4 space-y-6">
<div>
<h4 class="font-medium mb-3 text-primary-400">Préférences d'affichage</h4>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium mb-1">Thème</label>
<select id="theme-select" class="w-full bg-gray-700 text-white rounded-lg py-2 px-3">
<option value="dark">Sombre</option>
<option value="light">Clair</option>
<option value="system">Système</option>
</select>
</div>
<div>
<label class="block text-sm font-medium mb-1">Langue</label>
<select id="language-select" class="w-full bg-gray-700 text-white rounded-lg py-2 px-3">
<option value="fr">Français</option>
<option value="en">Anglais</option>
<option value="es">Espagnol</option>
<option value="de">Allemand</option>
<option value="it">Italien</option>
</select>
</div>
</div>
</div>
<div>
<h4 class="font-medium mb-3 text-primary-400">Expérience de chat</h4>
<div class="space-y-3">
<div class="flex items-center justify-between">
<label class="flex items-center space-x-2">
<span>Indicateur de saisie</span>
</label>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" id="typing-indicator" class="sr-only peer">
<div class="w-9 h-5 bg-gray-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] 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-600"></div>
</label>
</div>
<div class="flex items-center justify-between">
<label class="flex items-center space-x-2">
<span>Sons des messages</span>
</label>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" id="message-sounds" class="sr-only peer">
<div class="w-9 h-5 bg-gray-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] 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-600"></div>
</label>
</div>
<div class="flex items-center justify-between">
<label class="flex items-center space-x-2">
<span>Suggestions automatiques</span>
</label>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" id="auto-suggestions" class="sr-only peer">
<div class="w-9 h-5 bg-gray-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] 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-600"></div>
</label>
</div>
</div>
</div>
<div>
<h4 class="font-medium mb-3 text-primary-400">Paramètres avancés</h4>
<div>
<label class="block text-sm font-medium mb-1">Modèle IA</label>
<select id="model-select" class="w-full bg-gray-700 text-white rounded-lg py-2 px-3">
<option value="nous-hermes-2-mistral-7b-dpo">Nous-Hermes-2-Mistral-7B-DPO</option>
<option value="nous-hermes-2-mixtral-8x7b-dpo">Nous-Hermes-2-Mixtral-8x7B-DPO</option>
<option value="mistral-7b-instruct">Mistral-7B-Instruct</option>
<option value="mixtral-8x7b-instruct">Mixtral-8x7B-Instruct</option>
</select>
<p class="text-xs text-gray-500 mt-1">Le modèle affecte la qualité et la vitesse des réponses.</p>
</div>
</div>
</div>
<div class="p-4 border-t border-gray-700 flex justify-between">
<button id="reset-settings" class="text-gray-400 hover:text-white text-sm">
Réinitialiser les paramètres
</button>
<button id="save-settings" class="bg-primary-600 hover:bg-primary-700 text-white py-2 px-6 rounded-lg transition">
Enregistrer
</button>
</div>
</div>
</div>
<!-- Help Modal -->
<div id="help-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
<div class="bg-gray-800 rounded-lg w-full max-w-2xl max-h-[90vh] overflow-y-auto">
<div class="p-4 border-b border-gray-700 flex justify-between items-center">
<h3 class="text-lg font-semibold">Aide & Fonctionnalités</h3>
<button id="close-help" class="text-gray-400 hover:text-white">
<i class="fas fa-times"></i>
</button>
</div>
<div class="p-4 space-y-6">
<div class="bg-gray-700 rounded-lg p-4">
<h4 class="font-medium text-primary-400 mb-3">Comment utiliser Cygnis</h4>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="flex items-start space-x-3">
<div class="w-8 h-8 rounded-full bg-primary-600 flex items-center justify-center flex-shrink-0">
<i class="fas fa-comment text-white text-sm"></i>
</div>
<div>
<h5 class="font-medium">Envoyer un message</h5>
<p class="text-sm text-gray-400">Tapez dans la zone de texte et appuyez sur Entrée ou cliquez sur l'icône d'envoi.</p>
</div>
</div>
<div class="flex items-start space-x-3">
<div class="w-8 h-8 rounded-full bg-primary-600 flex items-center justify-center flex-shrink-0">
<i class="fas fa-plus text-white text-sm"></i>
</div>
<div>
<h5 class="font-medium">Nouvelle conversation</h5>
<p class="text-sm text-gray-400">Cliquez sur "Nouvelle conversation" dans la barre latérale.</p>
</div>
</div>
<div class="flex items-start space-x-3">
<div class="w-8 h-8 rounded-full bg-primary-600 flex items-center justify-center flex-shrink-0">
<i class="fas fa-history text-white text-sm"></i>
</div>
<div>
<h5 class="font-medium">Historique</h5>
<p class="text-sm text-gray-400">Accédez à vos conversations précédentes dans la liste à gauche.</p>
</div>
</div>
<div class="flex items-start space-x-3">
<div class="w-8 h-8 rounded-full bg-primary-600 flex items-center justify-center flex-shrink-0">
<i class="fas fa-trash text-white text-sm"></i>
</div>
<div>
<h5 class="font-medium">Suppression</h5>
<p class="text-sm text-gray-400">Supprimez une conversation en cliquant sur l'icône corbeille.</p>
</div>
</div>
</div>
</div>
<div>
<h4 class="font-medium text-primary-400 mb-3">Commandes utiles</h4>
<div class="grid grid-cols-1 md:grid-cols-3 gap-2">
<div class="bg-gray-700 rounded-lg p-3">
<div class="font-mono text-sm bg-gray-800 px-2 py-1 rounded mb-1">/reset</div>
<p class="text-xs text-gray-400">Réinitialise la conversation actuelle</p>
</div>
<div class="bg-gray-700 rounded-lg p-3">
<div class="font-mono text-sm bg-gray-800 px-2 py-1 rounded mb-1">/code</div>
<p class="text-xs text-gray-400">Formate la réponse en code</p>
</div>
<div class="bg-gray-700 rounded-lg p-3">
<div class="font-mono text-sm bg-gray-800 px-2 py-1 rounded mb-1">/explain</div>
<p class="text-xs text-gray-400">Explication détaillée</p>
</div>
<div class="bg-gray-700 rounded-lg p-3">
<div class="font-mono text-sm bg-gray-800 px-2 py-1 rounded mb-1">/summarize</div>
<p class="text-xs text-gray-400">Résume le texte fourni</p>
</div>
<div class="bg-gray-700 rounded-lg p-3">
<div class="font-mono text-sm bg-gray-800 px-2 py-1 rounded mb-1">/translate</div>
<p class="text-xs text-gray-400">Traduit le texte</p>
</div>
<div class="bg-gray-700 rounded-lg p-3">
<div class="font-mono text-sm bg-gray-800 px-2 py-1 rounded mb-1">/help</div>
<p class="text-xs text-gray-400">Affiche l'aide</p>
</div>
</div>
</div>
<div>
<h4 class="font-medium text-primary-400 mb-3">Fonctionnalités avancées</h4>
<div class="bg-gray-700 rounded-lg p-4">
<div class="flex items-start space-x-3 mb-4">
<div class="w-8 h-8 rounded-full bg-primary-600 flex items-center justify-center flex-shrink-0">
<i class="fas fa-code text-white text-sm"></i>
</div>
<div>
<h5 class="font-medium">Génération de code</h5>
<p class="text-sm text-gray-400">Cygnis peut générer du code dans divers langages de programmation et expliquer son fonctionnement.</p>
</div>
</div>
<div class="flex items-start space-x-3 mb-4">
<div class="w-8 h-8 rounded-full bg-primary-600 flex items-center justify-center flex-shrink-0">
<i class="fas fa-book text-white text-sm"></i>
</div>
<div>
<h5 class="font-medium">Rédaction de contenu</h5>
<p class="text-sm text-gray-400">Créez des articles, des emails, des poèmes et bien plus avec des instructions précises.</p>
</div>
</div>
<div class="flex items-start space-x-3">
<div class="w-8 h-8 rounded-full bg-primary-600 flex items-center justify-center flex-shrink-0">
<i class="fas fa-brain text-white text-sm"></i>
</div>
<div>
<h5 class="font-medium">Mémoire contextuelle</h5>
<p class="text-sm text-gray-400">Cygnis conserve le contexte de la conversation pour des réponses cohérentes.</p>
</div>
</div>
</div>
</div>
</div>
<div class="p-4 border-t border-gray-700">
<button id="close-help-btn" class="w-full bg-primary-600 hover:bg-primary-700 text-white py-2 px-4 rounded-lg transition">
Fermer
</button>
</div>
</div>
</div>
<!-- Edit Title Modal -->
<div id="edit-title-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
<div class="bg-gray-800 rounded-lg w-full max-w-md">
<div class="p-4 border-b border-gray-700 flex justify-between items-center">
<h3 class="text-lg font-semibold">Modifier le titre</h3>
<button id="close-edit-title" class="text-gray-400 hover:text-white">
<i class="fas fa-times"></i>
</button>
</div>
<div class="p-4">
<input
type="text"
id="conversation-title-input"
class="w-full bg-gray-700 text-white rounded-lg py-2 px-3 focus:outline-none focus:ring-2 focus:ring-primary-600"
placeholder="Entrez un nouveau titre"
>
</div>
<div class="p-4 border-t border-gray-700 flex justify-end">
<button id="cancel-edit-title" class="text-gray-400 hover:text-white mr-3">
Annuler
</button>
<button id="save-title-btn" class="bg-primary-600 hover:bg-primary-700 text-white py-2 px-6 rounded-lg transition">
Enregistrer
</button>
</div>
</div>
</div>
<script>
// Configuration de l'API Hugging Face
const MODEL_PATH = "mistralai/Mistral-7B-Instruct-v0.1";
const API_URL = 'https://needlessly-faithful-gopher.ngrok-free.app/generate';
// DOM Elements
const chatMessages = document.getElementById('chat-messages');
const messageInput = document.getElementById('message-input');
const sendBtn = document.getElementById('send-btn');
const newChatBtn = document.getElementById('new-chat-btn');
const conversationList = document.getElementById('conversation-list');
const currentConversationTitle = document.getElementById('current-conversation-title');
const editTitleBtn = document.getElementById('edit-title-btn');
const settingsBtn = document.getElementById('settings-btn');
const helpBtn = document.getElementById('help-btn');
const suggestionsBtn = document.getElementById('suggestions-btn');
const suggestionsPanel = document.getElementById('suggestions-panel');
const settingsModal = document.getElementById('settings-modal');
const helpModal = document.getElementById('help-modal');
const editTitleModal = document.getElementById('edit-title-modal');
const closeSettings = document.getElementById('close-settings');
const closeHelp = document.getElementById('close-help');
const closeHelpBtn = document.getElementById('close-help-btn');
const closeEditTitle = document.getElementById('close-edit-title');
const cancelEditTitle = document.getElementById('cancel-edit-title');
const saveTitleBtn = document.getElementById('save-title-btn');
const saveSettingsBtn = document.getElementById('save-settings');
const resetSettingsBtn = document.getElementById('reset-settings');
const searchConversations = document.getElementById('search-conversations');
const userAvatar = document.getElementById('user-avatar');
const usernameElement = document.getElementById('username');
const attachBtn = document.getElementById('attach-btn');
// State
let conversations = JSON.parse(localStorage.getItem('conversations')) || [];
let currentConversationId = localStorage.getItem('currentConversationId') || null;
let settings = JSON.parse(localStorage.getItem('settings')) || {
theme: 'dark',
language: 'fr',
typingIndicator: true,
messageSounds: true,
autoSuggestions: true,
model: 'nous-hermes-2-mistral-7b-dpo'
};
// Initialize
function init() {
applySettings();
renderConversationList();
// Load current conversation or create new one
if (currentConversationId) {
const conversation = conversations.find(c => c.id === currentConversationId);
if (conversation) {
loadConversation(conversation.id);
} else {
createNewConversation();
}
} else {
createNewConversation();
}
// Load user data
const urlParams = new URLSearchParams(window.location.search);
const user = urlParams.get('user') || 'Invité';
const token = urlParams.get('token');
usernameElement.textContent = user;
if (token) {
userAvatar.innerHTML = `<img src="https://api.dicebear.com/6.x/identicon/svg?seed=${user}" alt="avatar" class="w-full h-full rounded-full">`;
}
}
// Apply settings
function applySettings() {
// Theme
if (settings.theme === 'dark' || (settings.theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
// Language
document.documentElement.lang = settings.language;
// Auto suggestions
if (settings.autoSuggestions) {
suggestionsPanel.classList.remove('hidden');
} else {
suggestionsPanel.classList.add('hidden');
}
}
// Save settings
function saveSettings() {
settings = {
theme: document.getElementById('theme-select').value,
language: document.getElementById('language-select').value,
typingIndicator: document.getElementById('typing-indicator').checked,
messageSounds: document.getElementById('message-sounds').checked,
autoSuggestions: document.getElementById('auto-suggestions').checked,
model: document.getElementById('model-select').value
};
localStorage.setItem('settings', JSON.stringify(settings));
applySettings();
closeModal(settingsModal);
// Show confirmation
showToast('Paramètres enregistrés avec succès');
}
// Reset settings to default
function resetSettings() {
if (confirm('Voulez-vous vraiment réinitialiser tous les paramètres aux valeurs par défaut ?')) {
settings = {
theme: 'dark',
language: 'fr',
typingIndicator: true,
messageSounds: true,
autoSuggestions: true,
model: 'nous-hermes-2-mistral-7b-dpo'
};
localStorage.setItem('settings', JSON.stringify(settings));
applySettings();
// Update form fields
document.getElementById('theme-select').value = settings.theme;
document.getElementById('language-select').value = settings.language;
document.getElementById('typing-indicator').checked = settings.typingIndicator;
document.getElementById('message-sounds').checked = settings.messageSounds;
document.getElementById('auto-suggestions').checked = settings.autoSuggestions;
document.getElementById('model-select').value = settings.model;
showToast('Paramètres réinitialisés');
}
}
// Create new conversation
function createNewConversation() {
const id = Date.now().toString();
const newConvo = {
id,
title: `Conversation ${conversations.length + 1}`,
messages: [],
createdAt: new Date().toISOString()
};
conversations.unshift(newConvo);
currentConversationId = id;
saveConversations();
renderConversationList();
loadConversation(id);
clearChatMessages();
currentConversationTitle.textContent = newConvo.title;
// Show welcome message if it's a brand new conversation
if (conversations.length === 1) {
showWelcomeMessage();
}
}
// Show welcome message
function showWelcomeMessage() {
const welcomeHTML = `
<div class="max-w-3xl mx-auto text-center py-10">
<div class="w-16 h-16 mx-auto rounded-full bg-primary-600 flex items-center justify-center mb-4">
<i class="fas fa-robot text-2xl text-white"></i>
</div>
<h2 class="text-2xl font-bold mb-2">
<span class="wave-animation">
<span style="--i:1">B</span>
<span style="--i:2">o</span>
<span style="--i:3">n</span>
<span style="--i:4">j</span>
<span style="--i:5">o</span>
<span style="--i:6">u</span>
<span style="--i:7">r</span>
<span style="--i:8">!</span>
</span>
</h2>
<p class="text-gray-400 mb-6">Je suis Cygnis, votre assistant IA basé sur Nous-Hermes-2-Mistral-7B-DPO. Comment puis-je vous aider aujourd'hui ?</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 max-w-xl mx-auto">
<button class="suggestion-chip bg-gray-800 hover:bg-gray-700 py-2 px-4 rounded-lg text-left transition">
<div class="font-medium">Expliquer un concept technique</div>
<div class="text-xs text-gray-400">"Qu'est-ce qu'un réseau de neurones ?"</div>
</button>
<button class="suggestion-chip bg-gray-800 hover:bg-gray-700 py-2 px-4 rounded-lg text-left transition">
<div class="font-medium">Générer du code</div>
<div class="text-xs text-gray-400">"Montre-moi un exemple de composant React"</div>
</button>
<button class="suggestion-chip bg-gray-800 hover:bg-gray-700 py-2 px-4 rounded-lg text-left transition">
<div class="font-medium">Rédiger du contenu</div>
<div class="text-xs text-gray-400">"Écris un article sur les bienfaits du yoga"</div>
</button>
<button class="suggestion-chip bg-gray-800 hover:bg-gray-700 py-2 px-4 rounded-lg text-left transition">
<div class="font-medium">Résoudre un problème</div>
<div class="text-xs text-gray-400">"Comment optimiser mes performances SQL ?"</div>
</button>
</div>
</div>
`;
chatMessages.innerHTML = welcomeHTML;
// Add event listeners to suggestion chips
document.querySelectorAll('.suggestion-chip').forEach(chip => {
chip.addEventListener('click', () => {
const question = chip.querySelector('.text-xs').textContent.replace(/^"|"$/g, '');
messageInput.value = question;
messageInput.focus();
});
});
}
// Load conversation
function loadConversation(id) {
const conversation = conversations.find(c => c.id === id);
if (!conversation) return;
currentConversationId = id;
localStorage.setItem('currentConversationId', id);
currentConversationTitle.textContent = conversation.title;
clearChatMessages();
if (conversation.messages.length === 0) {
showWelcomeMessage();
} else {
conversation.messages.forEach(message => {
appendMessage(message.role, message.content, false, true);
});
}
// Highlight active conversation in list
document.querySelectorAll('.conversation-item').forEach(item => {
item.classList.toggle('bg-gray-700', item.dataset.id === id);
});
// Scroll to bottom
setTimeout(() => {
chatMessages.scrollTop = chatMessages.scrollHeight;
}, 10);
}
// Save conversations to localStorage
function saveConversations() {
localStorage.setItem('conversations', JSON.stringify(conversations));
localStorage.setItem('currentConversationId', currentConversationId);
}
// Render conversation list
function renderConversationList() {
conversationList.innerHTML = '';
if (conversations.length === 0) {
conversationList.innerHTML = '<p class="text-gray-500 text-sm p-2 text-center">Aucune conversation</p>';
return;
}
conversations.forEach(conversation => {
const convoElement = document.createElement('div');
convoElement.className = `conversation-item p-2 rounded-lg cursor-pointer flex items-center justify-between ${conversation.id === currentConversationId ? 'bg-gray-700' : 'hover:bg-gray-700'}`;
convoElement.dataset.id = conversation.id;
// Format date
const date = new Date(conversation.createdAt);
const formattedDate = date.toLocaleDateString('fr-FR', {
day: 'numeric',
month: 'short',
hour: '2-digit',
minute: '2-digit'
});
convoElement.innerHTML = `
<div class="flex items-center space-x-2 truncate w-full">
<i class="fas fa-comment text-gray-400 flex-shrink-0"></i>
<div class="truncate flex-1">
<div class="truncate">${conversation.title}</div>
<div class="text-xs text-gray-500">${formattedDate}</div>
</div>
</div>
<button class="delete-convo text-gray-400 hover:text-red-400 p-1" data-id="${conversation.id}">
<i class="fas fa-trash text-xs"></i>
</button>
`;
convoElement.addEventListener('click', () => loadConversation(conversation.id));
const deleteBtn = convoElement.querySelector('.delete-convo');
deleteBtn.addEventListener('click', (e) => {
e.stopPropagation();
deleteConversation(conversation.id);
});
conversationList.appendChild(convoElement);
});
}
// Delete conversation
function deleteConversation(id) {
if (confirm('Voulez-vous vraiment supprimer cette conversation ?')) {
conversations = conversations.filter(c => c.id !== id);
if (currentConversationId === id) {
if (conversations.length > 0) {
loadConversation(conversations[0].id);
} else {
createNewConversation();
}
}
saveConversations();
renderConversationList();
showToast('Conversation supprimée');
}
}
// Edit conversation title
function editConversationTitle() {
const conversation = conversations.find(c => c.id === currentConversationId);
if (!conversation) return;
conversationTitleInput.value = conversation.title;
openModal(editTitleModal);
conversationTitleInput.focus();
}
// Save conversation title
function saveConversationTitle() {
const newTitle = conversationTitleInput.value.trim();
if (!newTitle) return;
const conversation = conversations.find(c => c.id === currentConversationId);
if (conversation) {
conversation.title = newTitle;
currentConversationTitle.textContent = newTitle;
saveConversations();
renderConversationList();
closeModal(editTitleModal);
showToast('Titre mis à jour');
}
}
// Clear chat messages
function clearChatMessages() {
chatMessages.innerHTML = '';
}
// Append message to chat
function appendMessage(role, content, isTyping = false, noAnimation = false) {
const messageElement = document.createElement('div');
messageElement.className = `message-transition flex ${role === 'user' ? 'justify-end' : 'justify-start'}`;
if (!noAnimation) {
messageElement.classList.add('message-enter');
setTimeout(() => {
messageElement.classList.add('message-enter-active');
}, 10);
} else {
messageElement.classList.add('message-enter-active');
}
if (isTyping) {
messageElement.innerHTML = `
<div class="bg-gray-700 rounded-lg p-3 max-w-[80%]">
<div class="typing-indicator">
<div class="typing-dot"></div>
<div class="typing-dot"></div>
<div class="typing-dot"></div>
</div>
</div>
`;
} else {
const messageContent = role === 'assistant' ? markdownToHtml(content) : content;
const timestamp = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
messageElement.innerHTML = `
<div class="message-container relative ${role === 'user' ? 'bg-primary-600' : 'bg-gray-700'} rounded-lg p-3 max-w-[80%]">
<div class="markdown-content">${messageContent}</div>
<div class="text-xs text-gray-400 mt-1 text-right">${timestamp}</div>
<div class="message-toolbar absolute -top-3 right-2 bg-gray-800 rounded-full p-1 shadow-lg">
<button class="copy-btn text-gray-400 hover:text-white p-1" title="Copier">
<i class="fas fa-copy text-xs"></i>
</button>
${role === 'assistant' ? `
<button class="regenerate-btn text-gray-400 hover:text-white p-1" title="Régénérer">
<i class="fas fa-sync-alt text-xs"></i>
</button>
` : ''}
</div>
</div>
`;
}
chatMessages.appendChild(messageElement);
// Add event listeners to message buttons
if (!isTyping) {
const copyBtn = messageElement.querySelector('.copy-btn');
if (copyBtn) {
copyBtn.addEventListener('click', () => {
const textToCopy = role === 'assistant' ? content : messageElement.querySelector('.markdown-content').textContent;
navigator.clipboard.writeText(textToCopy);
showToast('Message copié');
});
}
const regenerateBtn = messageElement.querySelector('.regenerate-btn');
if (regenerateBtn && role === 'assistant') {
regenerateBtn.addEventListener('click', () => {
const conversation = conversations.find(c => c.id === currentConversationId);
if (conversation) {
// Find the user message that prompted this response
const messageIndex = conversation.messages.findIndex(m => m.content === content && m.role === 'assistant');
if (messageIndex > 0 && conversation.messages[messageIndex - 1].role === 'user') {
const userMessage = conversation.messages[messageIndex - 1].content;
// Remove the old assistant response
conversation.messages.splice(messageIndex, 1);
saveConversations();
// Remove the message from UI
messageElement.remove();
// Resend the user message
messageInput.value = userMessage;
sendMessage();
}
}
});
}
}
// Scroll to bottom
setTimeout(() => {
chatMessages.scrollTop = chatMessages.scrollHeight;
}, 10);
return messageElement;
}
// Markdown to HTML (improved)
function markdownToHtml(markdown) {
if (!markdown) return '';
// Headers
markdown = markdown.replace(/^# (.*$)/gm, '<h1>$1</h1>');
markdown = markdown.replace(/^## (.*$)/gm, '<h2>$1</h2>');
markdown = markdown.replace(/^### (.*$)/gm, '<h3>$1</h3>');
// Bold and italic
markdown = markdown.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
markdown = markdown.replace(/\*(.*?)\*/g, '<em>$1</em>');
markdown = markdown.replace(/_(.*?)_/g, '<em>$1</em>');
// Links
markdown = markdown.replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
// Code blocks
markdown = markdown.replace(/```([a-z]*)\n([\s\S]*?)```/g, function(match, lang, code) {
return `<pre><code class="language-${lang}">${code.trim()}</code></pre>`;
});
markdown = markdown.replace(/`(.*?)`/g, '<code>$1</code>');
// Lists
markdown = markdown.replace(/^\s*\*\s(.*$)/gm, '<li>$1</li>');
markdown = markdown.replace(/^\s*-\s(.*$)/gm, '<li>$1</li>');
markdown = markdown.replace(/^\s*\+\s(.*$)/gm, '<li>$1</li>');
markdown = markdown.replace(/^\s*\d+\.\s(.*$)/gm, '<li>$1</li>');
markdown = markdown.replace(/(<li>.*<\/li>)+/g, '<ul>$&</ul>');
// Images
markdown = markdown.replace(/!\[(.*?)\]\((.*?)\)/g, '<img src="$2" alt="$1" class="max-w-full rounded-lg my-2">');
// Tables
markdown = markdown.replace(/^\|(.+)\|$\n^\|([\-: ]+\|)+$\n((?:^\|.+\|$\n?)+)/gm, function(match, headers, align, rows) {
headers = headers.split('|').map(h => h.trim());
align = align.split('|').map(a => a.trim());
rows = rows.split('\n').filter(r => r.trim());
let html = '<table class="markdown-table"><thead><tr>';
// Headers
headers.forEach(h => {
if (h) html += `<th>${h}</th>`;
});
html += '</tr></thead><tbody>';
// Rows
rows.forEach(r => {
const cells = r.split('|').map(c => c.trim());
html += '<tr>';
cells.forEach(c => {
if (c) html += `<td>${c}</td>`;
});
html += '</tr>';
});
html += '</tbody></table>';
return html;
});
// Blockquotes
markdown = markdown.replace(/^>\s(.*$)/gm, '<blockquote>$1</blockquote>');
// Horizontal rule
markdown = markdown.replace(/^\-\-\-$/gm, '<hr class="border-gray-600 my-3">');
// Paragraphs
markdown = markdown.replace(/^(?!<[a-z])(.*$)/gm, function(m) {
return m.trim() ? '<p>' + m + '</p>' : '';
});
// Line breaks
markdown = markdown.replace(/\n/g, '<br>');
return markdown;
}
// Query Hugging Face API
async function queryHuggingFace(prompt) {
try {
const response = await fetch(API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
inputs: prompt,
parameters: {
max_new_tokens: 1000,
temperature: 0.7,
top_p: 0.9,
repetition_penalty: 1.1,
return_full_text: false
}
})
});
if (!response.ok) {
throw new Error(`Erreur HTTP: ${response.status}`);
}
const result = await response.json();
return result.generated_text; // on suppose que Flask retourne {"generated_text": "..."}
} catch (error) {
console.error('Erreur lors de la requête à l\'API locale Flask:', error);
return "Désolé, une erreur s'est produite lors de la génération de la réponse. Veuillez réessayer.";
}
}
// Send message
async function sendMessage() {
const message = messageInput.value.trim();
if (!message) return;
// Add user message to chat
appendMessage('user', message);
// Add to current conversation
const conversation = conversations.find(c => c.id === currentConversationId);
if (conversation) {
conversation.messages.push({
role: 'user',
content: message,
timestamp: new Date().toISOString()
});
// Update conversation title if it's the first message
if (conversation.messages.length === 1) {
conversation.title = message.length > 30 ? message.substring(0, 30) + '...' : message;
currentConversationTitle.textContent = conversation.title;
renderConversationList();
}
}
// Clear input
messageInput.value = '';
messageInput.style.height = 'auto';
// Show typing indicator
const typingElement = appendMessage('assistant', '', true);
try {
// Get conversation history for context
const conversationHistory = conversation ? conversation.messages : [];
// Format the prompt with conversation history
let prompt = message;
if (conversationHistory.length > 1) {
prompt = conversationHistory.map(m =>
m.role === 'user' ? `[INST] ${m.content} [/INST]` : m.content
).join('\n') + `\n[INST] ${message} [/INST]`;
}
// Call Hugging Face API
const responseText = await queryHuggingFace(prompt);
// Remove typing indicator
typingElement.remove();
// Add assistant message to chat
appendMessage('assistant', responseText);
// Add to current conversation
if (conversation) {
conversation.messages.push({
role: 'assistant',
content: responseText,
timestamp: new Date().toISOString()
});
}
saveConversations();
// Play sound if enabled
if (settings.messageSounds) {
const sound = new Audio('https://assets.mixkit.co/sfx/preview/mixkit-positive-interface-beep-221.mp3');
sound.volume = 0.3;
sound.play();
}
} catch (error) {
console.error('Error:', error);
typingElement.remove();
appendMessage('assistant', "Désolé, une erreur s'est produite. Veuillez réessayer.");
}
}
// Show toast notification
function showToast(message) {
const toast = document.createElement('div');
toast.className = 'fixed bottom-4 right-4 bg-gray-800 text-white px-4 py-2 rounded-lg shadow-lg flex items-center animate-pulse';
toast.innerHTML = `
<i class="fas fa-check-circle text-green-500 mr-2"></i>
<span>${message}</span>
`;
document.body.appendChild(toast);
setTimeout(() => {
toast.classList.remove('animate-pulse');
toast.style.opacity = '0';
toast.style.transition = 'opacity 0.5s ease';
setTimeout(() => {
toast.remove();
}, 500);
}, 2000);
}
// Open modal
function openModal(modal) {
modal.classList.remove('hidden');
document.body.style.overflow = 'hidden';
}
// Close modal
function closeModal(modal) {
modal.classList.add('hidden');
document.body.style.overflow = '';
}
// Search conversations
function searchConversationsHandler() {
const searchTerm = searchConversations.value.toLowerCase();
document.querySelectorAll('.conversation-item').forEach(item => {
const title = item.textContent.toLowerCase();
if (title.includes(searchTerm)) {
item.style.display = 'flex';
} else {
item.style.display = 'none';
}
});
}
// Event Listeners
sendBtn.addEventListener('click', sendMessage);
messageInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
// Auto-resize textarea
messageInput.addEventListener('input', () => {
messageInput.style.height = 'auto';
messageInput.style.height = `${Math.min(messageInput.scrollHeight, 200)}px`;
});
newChatBtn.addEventListener('click', createNewConversation);
editTitleBtn.addEventListener('click', editConversationTitle);
settingsBtn.addEventListener('click', () => {
// Load current settings into form
document.getElementById('theme-select').value = settings.theme;
document.getElementById('language-select').value = settings.language;
document.getElementById('typing-indicator').checked = settings.typingIndicator;
document.getElementById('message-sounds').checked = settings.messageSounds;
document.getElementById('auto-suggestions').checked = settings.autoSuggestions;
document.getElementById('model-select').value = settings.model;
openModal(settingsModal);
});
helpBtn.addEventListener('click', () => {
openModal(helpModal);
});
suggestionsBtn.addEventListener('click', () => {
suggestionsPanel.classList.toggle('hidden');
});
closeSettings.addEventListener('click', () => closeModal(settingsModal));
closeHelp.addEventListener('click', () => closeModal(helpModal));
closeHelpBtn.addEventListener('click', () => closeModal(helpModal));
closeEditTitle.addEventListener('click', () => closeModal(editTitleModal));
cancelEditTitle.addEventListener('click', () => closeModal(editTitleModal));
saveTitleBtn.addEventListener('click', saveConversationTitle);
saveSettingsBtn.addEventListener('click', saveSettings);
resetSettingsBtn.addEventListener('click', resetSettings);
// Click outside to close modals
window.addEventListener('click', (e) => {
if (e.target === settingsModal) closeModal(settingsModal);
if (e.target === helpModal) closeModal(helpModal);
if (e.target === editTitleModal) closeModal(editTitleModal);
});
// Search conversations
searchConversations.addEventListener('input', searchConversationsHandler);
// Add event listeners to suggestion chips
document.querySelectorAll('#suggestions-panel .suggestion-chip').forEach(chip => {
chip.addEventListener('click', () => {
messageInput.value = chip.textContent;
messageInput.focus();
});
});
// Initialize
init();
</script>
</body>
</html>