File size: 16,132 Bytes
f87680f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25ac863
f87680f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d510348
f87680f
25ac863
f87680f
 
 
 
 
 
 
 
 
 
 
 
 
 
25ac863
f87680f
 
 
 
 
 
 
 
 
 
25ac863
 
f87680f
 
 
 
 
 
 
 
 
 
 
25ac863
 
f87680f
 
 
 
 
 
 
 
25ac863
f87680f
 
 
 
 
 
 
 
 
25ac863
f87680f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25ac863
 
f87680f
 
 
 
 
25ac863
f87680f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25ac863
f87680f
a5874c3
f87680f
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Mariam AI! (Standalone)</title>
    <!-- Chargement de Tailwind CSS via CDN avec le plugin Forms -->
    <script src="https://cdn.tailwindcss.com?plugins=forms"></script>
    <!-- Favicon (Optionnel) -->
    <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🤖</text></svg>">
    <style>
        /* Styles de base pour assurer la hauteur complète et le défilement */
        html, body { height: 100%; margin: 0; padding: 0; }
        body { display: flex; flex-direction: column; font-family: Inter, sans-serif; /* Police sympa via CDN implicite de Tailwind */ }
        #chat-container { display: flex; flex-direction: column; flex-grow: 1; min-height: 0; } /* Crucial pour le scroll */
        #chat-messages { flex-grow: 1; overflow-y: auto; min-height: 0; } /* Crucial pour le scroll */

        /* Style pour la scrollbar (optionnel) */
        ::-webkit-scrollbar { width: 8px; }
        ::-webkit-scrollbar-track { background: #f1f1f1; border-radius: 10px; }
        ::-webkit-scrollbar-thumb { background: #a8a8a8; border-radius: 10px; }
        ::-webkit-scrollbar-thumb:hover { background: #7a7a7a; }

         /* Amélioration légère du rendu Markdown par défaut */
         .prose code { background-color: #e5e7eb; padding: 0.2em 0.4em; font-size: 85%; border-radius: 3px; }
         .prose pre > code { background-color: transparent; padding: 0; font-size: inherit; border-radius: 0; }
         .prose pre { background-color: #f3f4f6; padding: 1em; border-radius: 6px; overflow-x: auto; }
         .prose blockquote { border-left-color: #9ca3af; }
    </style>
</head>
<body class="bg-gray-100 flex flex-col h-screen">

    <!-- En-tête -->
    <header class="bg-gradient-to-r from-cyan-500 to-blue-500 text-white p-4 shadow-md flex justify-between items-center sticky top-0 z-10 flex-shrink-0">
        <h1 class="text-2xl font-bold">Mariam AI!</h1>
        <!-- Note: Le bouton clear fonctionne si le backend Flask a la route /clear -->
        <form action="/clear" method="POST" id="clear-form">
             <button type="submit" class="bg-red-500 hover:bg-red-600 text-white text-xs font-semibold py-1 px-3 rounded-full transition duration-200">
                Effacer Chat
             </button>
        </form>
    </header>

    <!-- Conteneur Principal du Chat -->
    <div id="chat-container" class="max-w-4xl w-full mx-auto bg-white shadow-xl rounded-b-lg flex flex-col flex-grow">

        <!-- Zone d'affichage des messages -->
        <div id="chat-messages" class="flex-grow overflow-y-auto p-6 space-y-4 scroll-smooth">
            <!-- Les messages seront ajoutés ici par JavaScript -->
            <!-- Message initial pour accueillir -->
             <div class="flex justify-start">
                <div class="bg-gray-200 text-gray-800 p-3 rounded-lg rounded-bl-none max-w-xs md:max-w-md shadow">
                    <div class="prose prose-sm max-w-none">Bonjour ! Comment puis-je vous aider aujourd'hui ?</div>
                </div>
            </div>

            <!-- Indicateur de chargement (caché par défaut) -->
            <div id="loading-indicator" class="text-center text-gray-500 italic py-4" style="display: none;">
                <div class="flex justify-center items-center space-x-2">
                    <div class="animate-spin rounded-full h-5 w-5 border-b-2 border-blue-500"></div>
                    <span>Mariam réfléchit...</span>
                </div>
            </div>
        </div>

        <!-- Zone d'erreur (cachée par défaut) -->
        <div id="error-message" class="bg-red-100 border-l-4 border-red-500 text-red-700 px-4 py-2 rounded m-4" role="alert" style="display: none;">
             <p class="font-bold">Erreur</p>
             <p id="error-text">Le message d'erreur ira ici.</p>
        </div>

         <!-- Barre d'options et d'upload -->
        <div class="bg-gray-50 border-t border-gray-200 px-4 py-2 flex-shrink-0">
             <div class="flex items-center justify-between text-sm">
                <label for="web_search_toggle" class="flex items-center space-x-2 cursor-pointer text-gray-600 hover:text-gray-800 select-none">
                    <input type="checkbox" id="web_search_toggle" name="web_search" value="true" class="form-checkbox h-4 w-4 rounded text-blue-500 focus:ring-blue-400 focus:ring-offset-0">
                    <span>Recherche Web</span>
                </label>
                <div class="flex items-center space-x-2">
                    <label for="file_upload" class="cursor-pointer text-blue-500 hover:text-blue-700 font-medium flex items-center">
                         <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
                            <path stroke-linecap="round" stroke-linejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
                         </svg>
                        <span>Fichier</span>
                        <input type="file" id="file_upload" name="file" class="hidden" accept=".jpg,.jpeg,.png,.pdf,.txt">
                    </label>
                     <span id="file-name" class="text-gray-500 text-xs truncate max-w-[100px]" title=""></span> {# Pour afficher le nom du fichier #}
                </div>
            </div>
        </div>

        <!-- Formulaire d'entrée -->
        <form id="chat-form" class="bg-gray-100 p-4 border-t border-gray-200 rounded-b-lg flex-shrink-0">
            <div class="flex items-center space-x-3">
                <input type="text" id="prompt" name="prompt" class="flex-grow form-input px-4 py-2 border border-gray-300 rounded-full focus:outline-none focus:ring-2 focus:ring-blue-400 shadow-sm" placeholder="Posez votre question à Mariam..." autocomplete="off">
                <button type="submit" id="send-button" class="bg-blue-500 hover:bg-blue-600 text-white font-bold p-2 rounded-full transition duration-200 flex items-center justify-center shadow-md w-10 h-10 flex-shrink-0">
                     <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
                       <path d="M10.894 2.553a1 1 0 00-1.788 0l-7 14a1 1 0 001.169 1.409l5-1.429A1 1 0 009 15.571V11a1 1 0 112 0v4.571a1 1 0 00.725.962l5 1.428a1 1 0 001.17-1.408l-7-14z" />
                     </svg>
                </button>
            </div>
        </form>

    </div>

    <!-- Script pour l'interaction -->
    <script>
    document.addEventListener('DOMContentLoaded', () => {
        const chatForm = document.getElementById('chat-form');
        const promptInput = document.getElementById('prompt');
        const chatMessages = document.getElementById('chat-messages');
        const loadingIndicator = document.getElementById('loading-indicator');
        const errorMessageDiv = document.getElementById('error-message');
        const errorTextP = document.getElementById('error-text');
        const webSearchToggle = document.getElementById('web_search_toggle');
        const fileUpload = document.getElementById('file_upload');
        const fileNameSpan = document.getElementById('file-name');
        const sendButton = document.getElementById('send-button');
        const clearForm = document.getElementById('clear-form'); // Référence au formulaire clear

        // URL de l'API Backend (ASSUREZ-VOUS QUE CELA CORRESPOND À VOTRE BACKEND FLASK)
        const API_ENDPOINT = '/api/chat'; // Ou l'URL complète si nécessaire
        const CLEAR_ENDPOINT = '/clear'; // Endpoint pour effacer

        // --- Fonctions Utilitaires ---

        function scrollToBottom() {
            setTimeout(() => {
                chatMessages.scrollTop = chatMessages.scrollHeight;
            }, 50);
        }

        function showLoading(show) {
            loadingIndicator.style.display = show ? 'block' : 'none';
            sendButton.disabled = show;
            promptInput.disabled = show;
            sendButton.classList.toggle('opacity-50', show);
            sendButton.classList.toggle('cursor-not-allowed', show);
             if (show) {
                // Déplacer l'indicateur de chargement à la fin de la liste des messages
                 chatMessages.appendChild(loadingIndicator);
                scrollToBottom(); // Assure que l'indicateur est visible
             }
        }

        function displayError(message) {
            errorTextP.textContent = message;
            errorMessageDiv.style.display = 'block';
            // Remonter pour que l'erreur soit visible
            errorMessageDiv.scrollIntoView({ behavior: 'smooth', block: 'center' });
            // Optionnel: Cacher après un délai
            // setTimeout(() => { errorMessageDiv.style.display = 'none'; }, 8000);
        }

        function addMessageToChat(role, text, isHtml = false) {
            // Cacher l'erreur si elle était affichée
            errorMessageDiv.style.display = 'none';

            const messageWrapper = document.createElement('div');
            messageWrapper.classList.add('flex', role === 'user' ? 'justify-end' : 'justify-start', 'mb-4'); // Ajout mb-4

            const bubbleDiv = document.createElement('div');
            bubbleDiv.classList.add('p-3', 'rounded-lg', 'max-w-xs', 'sm:max-w-md', 'md:max-w-lg', 'shadow-md'); // Augmenté max-w

            if (role === 'user') {
                bubbleDiv.classList.add('bg-blue-500', 'text-white', 'rounded-br-none');
                const paragraph = document.createElement('p');
                paragraph.classList.add('text-sm', 'break-words'); // Permet la césure
                paragraph.textContent = text; // Toujours utiliser textContent pour l'input utilisateur
                bubbleDiv.appendChild(paragraph);
            } else { // Assistant
                bubbleDiv.classList.add('bg-gray-200', 'text-gray-800', 'rounded-bl-none');
                const proseDiv = document.createElement('div');
                // Ajout de classes prose pour le formatage markdown potentiel
                proseDiv.classList.add('prose', 'prose-sm', 'max-w-none', 'text-gray-800',
                                       'prose-headings:text-gray-800', 'prose-a:text-blue-600',
                                       'prose-strong:text-gray-800', 'prose-code:text-red-600',
                                       'prose-blockquote:text-gray-600', 'break-words'); // Césure aussi
                if (isHtml) {
                    // ATTENTION: Suppose que `text` est du HTML SÛR venant du backend
                    proseDiv.innerHTML = text;
                } else {
                    proseDiv.textContent = text;
                }
                bubbleDiv.appendChild(proseDiv);
            }

            messageWrapper.appendChild(bubbleDiv);
            // Insérer avant l'indicateur de chargement pour qu'il reste en bas
            chatMessages.insertBefore(messageWrapper, loadingIndicator);

            scrollToBottom();
        }

        // --- Gestionnaires d'événements ---

        fileUpload.addEventListener('change', () => {
            if (fileUpload.files.length > 0) {
                const name = fileUpload.files[0].name;
                fileNameSpan.textContent = name.length > 15 ? name.substring(0, 12) + '...' : name; // Tronquer si trop long
                fileNameSpan.title = name; // Titre complet au survol
            } else {
                fileNameSpan.textContent = '';
                fileNameSpan.title = '';
            }
        });

        chatForm.addEventListener('submit', async (e) => {
            e.preventDefault();

            const prompt = promptInput.value.trim();
            const file = fileUpload.files[0];
            const useWebSearch = webSearchToggle.checked;

            if (!prompt && !file) {
                displayError("Veuillez entrer un message ou sélectionner un fichier.");
                return;
            }

            errorMessageDiv.style.display = 'none'; // Cacher ancienne erreur

            // Construire et afficher le message utilisateur
            let userMessageText = prompt;
            if (file) {
                 // Précéder le prompt du nom de fichier pour clarté dans l'UI
                 userMessageText = `[${file.name}] ${prompt}`;
             }
            // N'afficher que s'il y a du texte ou un fichier
            if (userMessageText || file) {
                 addMessageToChat('user', userMessageText || `[${file.name}]`); // Afficher nom fichier si prompt vide
            }


            const formData = new FormData();
            // Ajouter le prompt même s'il est vide si un fichier est présent
            formData.append('prompt', prompt);
            formData.append('web_search', useWebSearch);
            if (file) {
                formData.append('file', file);
            }

            showLoading(true);
            promptInput.value = ''; // Vider input texte
            fileUpload.value = ''; // Important pour pouvoir re-sélectionner le même fichier
            fileNameSpan.textContent = ''; // Vider nom fichier affiché
            fileNameSpan.title = '';

            try {
                const response = await fetch(API_ENDPOINT, {
                    method: 'POST',
                    body: formData,
                });

                const data = await response.json(); // Toujours essayer de lire le JSON

                if (!response.ok) {
                     // Utiliser le message d'erreur du JSON si disponible
                    throw new Error(data.error || `Erreur serveur: ${response.status}`);
                }

                if (data.success && data.message) {
                    // Assumer que data.message est du HTML sûr venant du backend
                    addMessageToChat('assistant', data.message, true); // true indique que c'est du HTML
                } else {
                    // Si success est true mais pas de message (peu probable) ou success est false
                    throw new Error(data.error || "Réponse invalide du serveur.");
                }

            } catch (error) {
                console.error("Erreur lors de l'envoi:", error);
                displayError(error.message || "Une erreur de connexion est survenue.");
            } finally {
                showLoading(false);
                promptInput.focus();
            }
        });

         // Optionnel : gestion du "clear" via JS pour éviter rechargement complet
         clearForm.addEventListener('submit', async (e) => {
             e.preventDefault(); // Empêche la soumission classique
             if (confirm("Voulez-vous vraiment effacer la conversation ?")) {
                 try {
                     const response = await fetch(CLEAR_ENDPOINT, { method: 'POST' });
                     if (response.ok) {
                         // Effacer les messages côté client
                         chatMessages.innerHTML = ''; // Vide la zone de chat
                          // Remettre le message d'accueil
                          addMessageToChat('assistant', "Conversation effacée. Comment puis-je vous aider ?");
                         // Vider aussi l'erreur potentielle
                         errorMessageDiv.style.display = 'none';
                         console.log("Chat effacé côté serveur.");
                     } else {
                         throw new Error("Impossible d'effacer côté serveur.");
                     }
                 } catch (error) {
                     console.error("Erreur lors de l'effacement:", error);
                     displayError("Erreur lors de l'effacement du chat.");
                 }
             }
         });


        // Scroll initial et focus
        scrollToBottom();
        promptInput.focus();
    });
    </script>

</body>
</html>