Spaces:
Sleeping
Sleeping
| <html lang="fr"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <meta name="description" | |
| content="Les Chats De SeaTech - Votre guide félin pour l'école d'ingénieurs de l'Université de Toulon"> | |
| <title>Les Chats De SeaTech</title> | |
| <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}"> | |
| <link rel="icon" href="{{ url_for('static', filename='img/seatech_logo.png') }}" type="image/png"> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css"> | |
| </head> | |
| <body> | |
| <div class="main-container"> | |
| <!-- Sidebar --> | |
| <!-- Contenu principal --> | |
| <main class="container"> | |
| <header class="header"> | |
| <div class="logo-container"> | |
| <img src="{{ url_for('static', filename='img/seatech_logo.png') }}" alt="Logo SeaTech" class="logo" | |
| width="250" height="auto"> | |
| </div> | |
| <h1>Les Chats De SeaTech</h1> | |
| <p class="subtitle">Votre guide félin pour l'école d'ingénieurs de l'Université de Toulon</p> | |
| </header> | |
| <!-- Section de sélection de rôle --> | |
| {% if not user_profile.get('confirmed') %} | |
| <section class="role-selection-container"> | |
| <div class="role-selection-header"> | |
| <h3><i class="fas fa-user-circle"></i> Choisissez votre profil</h3> | |
| <p>Pour mieux vous aider, veuillez sélectionner votre profil :</p> | |
| </div> | |
| <div class="role-buttons-grid"> | |
| <div class="role-button" data-role="Étudiant"> | |
| <div class="role-button-header"> | |
| <div class="role-icon"><i class="fas fa-graduation-cap"></i></div> | |
| <div class="role-title">Étudiant</div> | |
| </div> | |
| <div class="role-description"> | |
| Étudiant actuel de SeaTech cherchant des informations sur les cours, emplois du temps, | |
| projets, vie associative... | |
| </div> | |
| </div> | |
| <div class="role-button" data-role="Candidat"> | |
| <div class="role-button-header"> | |
| <div class="role-icon"><i class="fas fa-user-plus"></i></div> | |
| <div class="role-title">Candidat</div> | |
| </div> | |
| <div class="role-description"> | |
| Candidat intéressé par SeaTech : admissions, concours, formations, spécialités | |
| disponibles... | |
| </div> | |
| </div> | |
| <div class="role-button" data-role="Enseignant"> | |
| <div class="role-button-header"> | |
| <div class="role-icon"><i class="fas fa-chalkboard-teacher"></i></div> | |
| <div class="role-title">Enseignant</div> | |
| </div> | |
| <div class="role-description"> | |
| Enseignant cherchant des ressources pédagogiques, contacts administratifs, organisation des | |
| cours... | |
| </div> | |
| </div> | |
| <div class="role-button" data-role="Alumni"> | |
| <div class="role-button-header"> | |
| <div class="role-icon"><i class="fas fa-user-tie"></i></div> | |
| <div class="role-title">Alumni</div> | |
| </div> | |
| <div class="role-description"> | |
| Ancien étudiant de SeaTech intéressé par le réseau alumni, partenariats, événements... | |
| </div> | |
| </div> | |
| </div> | |
| <div class="confirm-role-section active"> | |
| <p><i class="fas fa-mouse-pointer"></i> Cliquez sur un profil pour le confirmer automatiquement.</p> | |
| </div> | |
| </section> | |
| {% else %} | |
| <!-- Affichage du rôle actuel --> | |
| <div class="current-role-display"> | |
| <p><i class="fas fa-user-check"></i> Profil actuel : <span class="role-name">{{ user_profile.role | |
| }}</span></p> | |
| <button class="reset-button" id="resetRoleBtn"> | |
| <i class="fas fa-redo"></i> Changer de profil | |
| </button> | |
| </div> | |
| {% endif %} | |
| <section class="chat-container"> | |
| <div class="chat-history" id="chatHistory"> | |
| <article class="message bot-message"> | |
| <div class="message-avatar"> | |
| <img src="{{ url_for('static', filename='img/franky.png') }}" alt="Franky" class="avatar" | |
| loading="lazy"> | |
| </div> | |
| <div class="message-content"> | |
| <div class="message-header"> | |
| <strong>Franky</strong> | |
| </div> | |
| <p>Miaaaou ! Je suis <strong>Franky</strong>, votre guide félin de SeaTech ! 🐱 Mon ami | |
| <strong>Freddy</strong> et moi sommes là pour vous aider. | |
| {% if not user_profile.get('confirmed') %} | |
| Commencez par sélectionner votre profil ci-dessus pour que je puisse adapter mes | |
| réponses à vos besoins. | |
| {% else %} | |
| Posez-moi n'importe quelle question sur l'école et je vous répondrai en tant | |
| qu'assistant spécialisé pour les {{ user_profile.role }}s. | |
| {% endif %} | |
| </p> | |
| </div> | |
| </article> | |
| {% if conversation %} | |
| {% for msg in conversation %} | |
| <article class="message {% if msg.role == 'user' %}user-message{% else %}bot-message{% endif %}"> | |
| {% if msg.role != 'user' %} | |
| <div class="message-avatar"> | |
| <img src="{{ url_for('static', filename='img/franky.png') }}" alt="Franky" class="avatar" | |
| loading="lazy"> | |
| </div> | |
| {% endif %} | |
| <div class="message-content"> | |
| {% if msg.role != 'user' %} | |
| <div class="message-header"> | |
| <strong>Franky</strong> | |
| <div class="message-actions"> | |
| {% if not msg.get('is_system_message') %} | |
| <button class="show-sources-btn" aria-label="Afficher les sources"><i | |
| class="fas fa-file-alt"></i> Sources</button> | |
| {% endif %} | |
| {% if msg.processing_time %} | |
| <span class="processing-time">{{ msg.processing_time }}</span> | |
| {% endif %} | |
| </div> | |
| </div> | |
| {% endif %} | |
| {{ msg.content | safe }} | |
| </div> | |
| {% if msg.role == 'user' %} | |
| <div class="message-avatar"> | |
| <img src="{{ url_for('static', filename='img/user.png') }}" alt="User" class="avatar" | |
| loading="lazy"> | |
| </div> | |
| {% endif %} | |
| </article> | |
| {% endfor %} | |
| {% else %} | |
| {% if not user_profile.get('confirmed') %} | |
| <div class="role-required-notice"> | |
| <i class="fas fa-info-circle"></i> Veuillez sélectionner votre profil ci-dessus pour commencer à | |
| poser vos questions. | |
| </div> | |
| {% else %} | |
| <div class="no-messages">Posez votre première question !</div> | |
| {% endif %} | |
| {% endif %} | |
| </div> | |
| <div class="loading" id="loadingIndicator"> | |
| <div class="loading-container"> | |
| <img src="{{ url_for('static', filename='img/searching-cat.gif') }}" alt="Chat qui cherche" | |
| class="searching-cat"> | |
| <span id="loadingText">Je demande à Freddy<span class="loading-dots"></span></span> | |
| </div> | |
| </div> | |
| <form class="chat-form{% if not user_profile.get('confirmed') %} disabled{% endif %}" id="chatForm" | |
| method="POST" action="/"> | |
| <div class="chat-input-container"> | |
| <img src="{{ url_for('static', filename='img/cat_walking.png') }}" alt="Chat qui marche" | |
| class="cat-walking"> | |
| <input type="text" name="query" id="queryInput" class="chat-input" | |
| placeholder="{% if user_profile.get('confirmed') %}Posez votre question sur SeaTech...{% else %}Sélectionnez d'abord votre profil ci-dessus{% endif %}" | |
| {% if not user_profile.get('confirmed') %}disabled{% endif %} required autocomplete="off" | |
| aria-label="Votre question"> | |
| </div> | |
| <!-- Bouton micro --> | |
| <button type="button" id="micButton" class="mic-button" {% if not user_profile.get('confirmed') | |
| %}disabled{% endif %}> | |
| <i class="fas fa-microphone"></i> | |
| </button> | |
| <!-- Bouton : mode conversation--> | |
| <button type="button" id="conversationBtn" class="conversation-button"> | |
| <i class="fas fa-comments"></i> Start conversation | |
| </button> | |
| <button type="submit" class="send-button" {% if not user_profile.get('confirmed') %}disabled{% endif | |
| %}> | |
| Envoyer | |
| <img src="{{ url_for('static', filename='img/cat-icon.png') }}" alt="Chat" class="cat-icon" | |
| width="20" height="20"> | |
| </button> | |
| </form> | |
| </section> | |
| </main> | |
| <!-- Panneau des sources (caché par défaut) --> | |
| <aside class="sources-panel" id="sourcesPanel" aria-hidden="true"> | |
| <div class="sources-header"> | |
| <h3>Sources consultées par Freddy</h3> | |
| <button id="closeSourcesBtn" class="close-btn" aria-label="Fermer le panneau des sources">×</button> | |
| </div> | |
| <div class="sources-content" id="sourcesContent"> | |
| <!-- Sources insérées dynamiquement --> | |
| </div> | |
| </aside> | |
| <!-- Panneau de Freddy (toujours visible) --> | |
| <aside class="freddy-panel card"> | |
| <div class="freddy-header"> | |
| <h3> | |
| <img src="{{ url_for('static', filename='img/freddy.png') }}" alt="Freddy" loading="lazy"> | |
| Recherche par Freddy | |
| </h3> | |
| </div> | |
| <div class="freddy-content"> | |
| <div class="freddy-intro"> | |
| <img src="{{ url_for('static', filename='img/freddy.png') }}" alt="Freddy" loading="lazy"> | |
| <div> | |
| <p><strong>Miaaaou !</strong> Je suis <strong>Freddy</strong>, l'assistant de recherche de | |
| SeaTech.</p> | |
| <p>Je fouille dans les documents pour trouver les informations les plus pertinentes | |
| {% if user_profile.get('confirmed') %}pour les {{ user_profile.role }}s{% endif %} !</p> | |
| </div> | |
| </div> | |
| <div class="freddy-logs-title"> | |
| <i class="fas fa-search"></i> Analyse de la recherche | |
| </div> | |
| <div id="freddyLogs" class="freddy-logs"> | |
| <div class="freddy-log-entry"> | |
| <span class="log-time">{{ current_datetime.strftime('%H:%M:%S') }}</span> | |
| {% if user_profile.get('confirmed') %} | |
| <span class="log-action">Prêt à chercher pour un profil {{ user_profile.role }}</span> | |
| {% else %} | |
| <span class="log-action">En attente de sélection de profil...</span> | |
| {% endif %} | |
| </div> | |
| </div> | |
| <div class="freddy-sources-title"> | |
| <i class="fas fa-file-alt"></i> Sources consultées | |
| </div> | |
| <div id="freddySources" class="freddy-sources-container"> | |
| <div class="freddy-source-block"> | |
| <div class="freddy-source-header"> | |
| <span class="source-name">Informations SeaTech</span> | |
| {% if user_profile.get('confirmed') %} | |
| <span class="medium-relevance">Filtrage par profil {{ user_profile.role }}...</span> | |
| {% else %} | |
| <span class="medium-relevance">En attente de profil...</span> | |
| {% endif %} | |
| </div> | |
| <div class="freddy-source-content"> | |
| {% if user_profile.get('confirmed') %} | |
| Prêt à chercher des informations spécifiquement adaptées pour les {{ user_profile.role }}s. | |
| {% else %} | |
| Sélectionnez votre profil pour que je puisse filtrer les informations selon vos besoins. | |
| {% endif %} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </aside> | |
| </div> | |
| <footer class="footer"> | |
| <p>© {{ current_datetime.year }} École d'ingénieurs SeaTech - Université de Toulon | Tous droits réservés | |
| </p> | |
| </footer> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', function () { | |
| let isConversationMode = false; // mode conversation activé/désactivé | |
| const isiOS = /iPad|iPhone|iPod/.test(navigator.userAgent) | |
| || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1); | |
| if (isiOS) { | |
| document.body.classList.add('ios-device'); | |
| } | |
| function updateAppViewportHeight() { | |
| const viewportHeight = window.visualViewport ? window.visualViewport.height : window.innerHeight; | |
| const vh = viewportHeight * 0.01; | |
| document.documentElement.style.setProperty('--app-vh', `${vh}px`); | |
| } | |
| updateAppViewportHeight(); | |
| window.addEventListener('resize', updateAppViewportHeight); | |
| if (window.visualViewport) { | |
| window.visualViewport.addEventListener('resize', updateAppViewportHeight); | |
| } | |
| // Smooth scroll vers la zone de chat après sélection de rôle | |
| if (sessionStorage.getItem('scroll_to_chat')) { | |
| sessionStorage.removeItem('scroll_to_chat'); | |
| const chatSection = document.querySelector('.chat-container'); | |
| if (chatSection) { | |
| setTimeout(() => { | |
| chatSection.scrollIntoView({ behavior: 'smooth', block: 'start' }); | |
| }, 150); | |
| } | |
| } | |
| const chatForm = document.getElementById('chatForm'); | |
| const chatHistory = document.getElementById('chatHistory'); | |
| const loadingIndicator = document.getElementById('loadingIndicator'); | |
| const queryInput = document.getElementById('queryInput'); | |
| const loadingText = document.getElementById('loadingText'); | |
| const sourcesPanel = document.getElementById('sourcesPanel'); | |
| const sourcesContent = document.getElementById('sourcesContent'); | |
| const closeSourcesBtn = document.getElementById('closeSourcesBtn'); | |
| const freddyLogs = document.getElementById('freddyLogs'); | |
| const freddySources = document.getElementById('freddySources'); | |
| const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; | |
| if (isiOS && queryInput) { | |
| queryInput.addEventListener('focus', function () { | |
| document.body.classList.add('keyboard-open'); | |
| setTimeout(() => { | |
| queryInput.scrollIntoView({ behavior: 'smooth', block: 'center' }); | |
| }, 120); | |
| }); | |
| queryInput.addEventListener('blur', function () { | |
| document.body.classList.remove('keyboard-open'); | |
| }); | |
| } | |
| // Éléments de sélection de rôle | |
| const roleButtons = document.querySelectorAll('.role-button'); | |
| const resetRoleBtn = document.getElementById('resetRoleBtn'); | |
| let selectedRole = null; | |
| let isSubmittingRole = false; | |
| const savedRole = localStorage.getItem("seatech_role"); | |
| if (savedRole) { | |
| selectedRole = savedRole; | |
| } | |
| const loadingMessages = [ | |
| "Freddy fouille dans les archives...", | |
| "Freddy déchiffre les acronymes de SeaTech... 🐱", | |
| "Freddy explore les données de l'école...", | |
| "Miaaaou! Freddy a repéré une information intéressante...", | |
| "Freddy chasse les informations pertinentes... 🐾", | |
| "Les moustaches de Freddy frémissent... il est sur une piste!" | |
| ]; | |
| function submitRoleSelection(role, isAutoSubmit = false) { | |
| if (!role || isSubmittingRole) return; | |
| isSubmittingRole = true; | |
| roleButtons.forEach(btn => { | |
| btn.style.pointerEvents = 'none'; | |
| }); | |
| console.log('Envoi de la sélection de rôle:', role); | |
| localStorage.setItem("seatech_role", role); | |
| fetch('/api/ask', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ role_selection: role }), | |
| credentials: 'include' | |
| }) | |
| .then(response => { | |
| console.log('Réponse status:', response.status); | |
| if (response.status === 429) { | |
| if (isAutoSubmit) { | |
| console.log('Trop de requêtes, réessai dans 5 secondes...'); | |
| sessionStorage.removeItem("auto_submit_attempted"); | |
| setTimeout(() => { | |
| window.location.reload(); | |
| }, 5000); | |
| return null; | |
| } | |
| throw new Error('Serveur temporairement occupé. Réessayez dans quelques secondes.'); | |
| } | |
| return response.json(); | |
| }) | |
| .then(data => { | |
| if (!data) return; | |
| console.log('Données reçues:', data); | |
| if (data.status === 'role_selected') { | |
| console.log('Rôle sélectionné avec succès, rechargement...'); | |
| sessionStorage.setItem('scroll_to_chat', '1'); | |
| setTimeout(() => { | |
| window.location.href = window.location.origin + '/'; | |
| }, 500); | |
| return; | |
| } | |
| throw new Error('Erreur lors de la sélection du rôle. Veuillez réessayer.'); | |
| }) | |
| .catch(error => { | |
| console.error('Erreur lors de la sélection de rôle:', error); | |
| if (!isAutoSubmit) { | |
| alert(error.message || 'Erreur réseau. Veuillez réessayer.'); | |
| } else { | |
| sessionStorage.removeItem("auto_submit_attempted"); | |
| } | |
| }) | |
| .finally(() => { | |
| isSubmittingRole = false; | |
| roleButtons.forEach(btn => { | |
| btn.style.pointerEvents = ''; | |
| }); | |
| }); | |
| } | |
| // Gestion de la sélection de rôle: confirmation immédiate au clic | |
| roleButtons.forEach(button => { | |
| button.addEventListener('click', function () { | |
| if (isSubmittingRole) return; | |
| roleButtons.forEach(btn => btn.classList.remove('selected')); | |
| this.classList.add('selected'); | |
| selectedRole = this.dataset.role; | |
| submitRoleSelection(selectedRole); | |
| }); | |
| }); | |
| // si on a déjà enregistré un rôle, envoyer automatiquement au back seulement si le rôle n'est pas encore confirmé | |
| // et qu'on n'a pas déjà essayé cette session | |
| const autoSubmitAttempted = sessionStorage.getItem("auto_submit_attempted"); | |
| if (selectedRole && document.querySelector('.role-selection-container') && !autoSubmitAttempted) { | |
| console.log('Role déjà présent en localStorage:', selectedRole); | |
| sessionStorage.setItem("auto_submit_attempted", "true"); | |
| submitRoleSelection(selectedRole, true); | |
| } | |
| if (resetRoleBtn) { | |
| resetRoleBtn.addEventListener('click', function () { | |
| console.log('Réinitialisation du rôle...'); | |
| resetRoleBtn.disabled = true; | |
| fetch('/api/reset-role', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| credentials: 'include' | |
| }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| console.log('Réponse réinitialisation:', data); | |
| if (data.status === 'role_reset') { | |
| setTimeout(() => { | |
| window.location.href = window.location.origin + '/'; | |
| }, 500); | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('Erreur lors de la réinitialisation:', error); | |
| resetRoleBtn.disabled = false; | |
| }); | |
| }); | |
| } | |
| function extractSources(html) { | |
| if (!html) return null; | |
| const parser = new DOMParser(); | |
| const doc = parser.parseFromString(html, 'text/html'); | |
| const sourcesContainer = doc.querySelector('.sources-container'); | |
| return sourcesContainer ? sourcesContainer.innerHTML : null; | |
| } | |
| function updateFreddyContent(html) { | |
| if (!html) return; | |
| const parser = new DOMParser(); | |
| const doc = parser.parseFromString(html, 'text/html'); | |
| const logsSection = doc.querySelector('.freddy-logs'); | |
| if (logsSection) freddyLogs.innerHTML = logsSection.innerHTML; | |
| const sourcesSection = doc.querySelector('.freddy-sources-container'); | |
| if (sourcesSection) freddySources.innerHTML = sourcesSection.innerHTML; | |
| } | |
| closeSourcesBtn.addEventListener('click', function () { | |
| sourcesPanel.classList.remove('active'); | |
| sourcesPanel.setAttribute('aria-hidden', 'true'); | |
| }); | |
| let messageIndex = 0; | |
| let loadingInterval; | |
| function updateLoadingMessage() { | |
| loadingText.innerHTML = `${loadingMessages[messageIndex]}<span class="loading-dots"></span>`; | |
| messageIndex = (messageIndex + 1) % loadingMessages.length; | |
| } | |
| function scrollToBottom() { | |
| chatHistory.scrollTop = chatHistory.scrollHeight; | |
| } | |
| scrollToBottom(); | |
| function initButtons() { | |
| document.querySelectorAll('.show-sources-btn').forEach(button => { | |
| button.addEventListener('click', function () { | |
| sourcesPanel.classList.add('active'); | |
| sourcesPanel.setAttribute('aria-hidden', 'false'); | |
| }); | |
| }); | |
| } | |
| initButtons(); | |
| if (chatForm) { | |
| chatForm.addEventListener('submit', function (e) { | |
| e.preventDefault(); | |
| const query = queryInput.value.trim(); | |
| if (!query) return; | |
| const userMessageDiv = document.createElement('article'); | |
| userMessageDiv.className = 'message user-message'; | |
| userMessageDiv.innerHTML = ` | |
| <div class="message-content">${query}</div> | |
| <div class="message-avatar"> | |
| <img src="${window.location.origin}/static/img/user.png" alt="User" class="avatar" loading="lazy"> | |
| </div> | |
| `; | |
| chatHistory.appendChild(userMessageDiv); | |
| scrollToBottom(); | |
| loadingIndicator.style.display = 'block'; | |
| messageIndex = 0; | |
| updateLoadingMessage(); | |
| loadingInterval = setInterval(updateLoadingMessage, 2000); | |
| fetch('/api/ask', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ query: query }), | |
| credentials: 'include' | |
| }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| clearInterval(loadingInterval); | |
| loadingIndicator.style.display = 'none'; | |
| if (data.status === 'role_required') { | |
| const errorDiv = document.createElement('article'); | |
| errorDiv.className = 'message bot-message'; | |
| errorDiv.innerHTML = ` | |
| <div class="message-avatar"> | |
| <img src="${window.location.origin}/static/img/franky.png" alt="Franky" class="avatar" loading="lazy"> | |
| </div> | |
| <div class="message-content"> | |
| <div class="message-header"><strong>Franky</strong></div> | |
| ${data.response} | |
| </div> | |
| `; | |
| chatHistory.appendChild(errorDiv); | |
| scrollToBottom(); | |
| return; | |
| } | |
| const sourcesHtml = data.sources || extractSources(data.response); | |
| let cleanResponse = data.response; | |
| if (sourcesHtml && cleanResponse.includes("sources-container")) { | |
| const tempDiv = document.createElement('div'); | |
| tempDiv.innerHTML = cleanResponse; | |
| const sourcesContainer = tempDiv.querySelector('.sources-container'); | |
| if (sourcesContainer) sourcesContainer.remove(); | |
| cleanResponse = tempDiv.innerHTML; | |
| } | |
| if (sourcesHtml) sourcesContent.innerHTML = sourcesHtml; | |
| if (data.freddy_logs) updateFreddyContent(data.freddy_logs); | |
| const botMessageDiv = document.createElement('article'); | |
| botMessageDiv.className = 'message bot-message'; | |
| botMessageDiv.innerHTML = ` | |
| <div class="message-avatar"> | |
| <img src="${window.location.origin}/static/img/franky.png" alt="Franky" class="avatar" loading="lazy"> | |
| </div> | |
| <div class="message-content"> | |
| <div class="message-header"> | |
| <strong>Franky</strong> | |
| <div class="message-actions"> | |
| ${sourcesHtml ? '<button class="show-sources-btn" aria-label="Afficher les sources"><i class="fas fa-file-alt"></i> Sources</button>' : ''} | |
| ${data.processing_time ? '<span class="processing-time">' + data.processing_time + '</span>' : ''} | |
| ${data.confirmed_role ? '<span class="role-indicator">Profil: ' + data.confirmed_role + '</span>' : ''} | |
| </div> | |
| </div> | |
| ${cleanResponse} | |
| </div> | |
| `; | |
| chatHistory.appendChild(botMessageDiv); | |
| queryInput.value = ''; | |
| queryInput.focus(); | |
| //ajout Daly | |
| // Lecture vocale de la réponse (TTS) | |
| // Lecture vocale seulement si mode conversation actif | |
| if (isConversationMode && 'speechSynthesis' in window && data.response) { | |
| let textToRead = data.response | |
| .replace(/<[^>]+>/g, ' ') | |
| .replace(/[^\w\sàâäçéèêëîïôöùûüÿñœ]/gi, '') | |
| .replace(/\s+/g, ' ') | |
| .trim(); | |
| const utterance = new SpeechSynthesisUtterance(textToRead); | |
| utterance.lang = "fr-FR"; | |
| utterance.rate = 1.0; | |
| utterance.pitch = 1.0; | |
| speechSynthesis.cancel(); | |
| speechSynthesis.speak(utterance); | |
| } | |
| //fin ajout | |
| scrollToBottom(); | |
| initButtons(); | |
| }) | |
| .catch(error => { | |
| console.error('Erreur:', error); | |
| clearInterval(loadingInterval); | |
| loadingIndicator.style.display = 'none'; | |
| const errorDiv = document.createElement('article'); | |
| errorDiv.className = 'message bot-message'; | |
| errorDiv.innerHTML = ` | |
| <div class="message-avatar"> | |
| <img src="${window.location.origin}/static/img/franky.png" alt="Franky" class="avatar" loading="lazy"> | |
| </div> | |
| <div class="message-content"> | |
| <div class="message-header"><strong>Franky</strong></div> | |
| <p>Désolé, une erreur s'est produite pendant la recherche. Veuillez réessayer.</p> | |
| </div> | |
| `; | |
| chatHistory.appendChild(errorDiv); | |
| scrollToBottom(); | |
| }); | |
| }); | |
| } | |
| // === Reconnaissance vocale === | |
| const micBtn = document.getElementById('micButton'); // utilise l'ID réel du bouton | |
| if (micBtn && SpeechRecognition) { | |
| const recognition = new SpeechRecognition(); | |
| recognition.lang = "fr-FR"; | |
| recognition.continuous = false; | |
| recognition.interimResults = false; | |
| micBtn.addEventListener('click', () => { | |
| recognition.start(); | |
| micBtn.classList.add("listening"); // effet visuel pendant l'écoute | |
| }); | |
| recognition.onresult = (event) => { | |
| let transcript = event.results[0][0].transcript; | |
| // Corrections automatiques pour SeaTech | |
| const corrections = { | |
| "steak": "SeaTech", | |
| "scitec": "SeaTech", | |
| "sitec": "SeaTech", | |
| "citech": "SeaTech", | |
| "citek": "SeaTech", | |
| "sutec": "SeaTech", | |
| "sitac": "SeaTech", | |
| }; | |
| for (const [wrong, correct] of Object.entries(corrections)) { | |
| const regex = new RegExp(`\\b${wrong}\\b`, "gi"); // cherche le mot entier, insensible à la casse | |
| transcript = transcript.replace(regex, correct); | |
| } | |
| queryInput.value = transcript; // place le texte corrigé dans la barre de saisie | |
| //chatForm.requestSubmit(); // si tu veux envoyer auto, tu décommentes | |
| micBtn.classList.remove("listening"); | |
| }; | |
| recognition.onerror = (event) => { | |
| console.error("Erreur vocale:", event.error); | |
| micBtn.classList.remove("listening"); | |
| }; | |
| recognition.onend = () => { | |
| micBtn.classList.remove("listening"); | |
| }; | |
| } else if (micBtn) { | |
| micBtn.disabled = true; | |
| micBtn.title = "Reconnaissance vocale non disponible sur ce navigateur"; | |
| } | |
| // === Mode conversation === | |
| const conversationBtn = document.getElementById("conversationBtn"); | |
| if (conversationBtn && SpeechRecognition) { | |
| const convRecognition = new SpeechRecognition(); | |
| convRecognition.lang = "fr-FR"; | |
| convRecognition.continuous = false; | |
| convRecognition.interimResults = false; | |
| // ✅ Toggle ON/OFF du mode conversation | |
| conversationBtn.addEventListener("click", () => { | |
| isConversationMode = !isConversationMode; | |
| if (isConversationMode) { | |
| conversationBtn.classList.add("active"); | |
| conversationBtn.innerHTML = '<i class="fas fa-comments"></i> Stop conversation'; | |
| convRecognition.start(); | |
| } else { | |
| conversationBtn.classList.remove("active"); | |
| conversationBtn.innerHTML = '<i class="fas fa-comments"></i> Start conversation'; | |
| convRecognition.stop(); | |
| speechSynthesis.cancel(); // stoppe toute lecture en cours | |
| } | |
| }); | |
| convRecognition.onresult = (event) => { | |
| let transcript = event.results[0][0].transcript; | |
| // Corrections automatiques pour SeaTech | |
| const corrections = { "steak": "SeaTech", "scitec": "SeaTech", "sitec": "SeaTech" }; | |
| for (const [wrong, correct] of Object.entries(corrections)) { | |
| const regex = new RegExp(`\\b${wrong}\\b`, "gi"); | |
| transcript = transcript.replace(regex, correct); | |
| } | |
| // Envoi automatique si mode conversation actif | |
| if (isConversationMode) { | |
| fetch('/api/ask', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ query: transcript, mode: "conversation" }), | |
| credentials: 'include' | |
| }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| const botMessageDiv = document.createElement('article'); | |
| botMessageDiv.className = 'message bot-message'; | |
| botMessageDiv.innerHTML = ` | |
| <div class="message-avatar"> | |
| <img src="${window.location.origin}/static/img/franky.png" alt="Franky" class="avatar" loading="lazy"> | |
| </div> | |
| <div class="message-content"> | |
| <div class="message-header"><strong>Franky</strong></div> | |
| ${data.response} | |
| </div>`; | |
| chatHistory.appendChild(botMessageDiv); | |
| scrollToBottom(); | |
| // ✅ Lecture vocale courte et propre | |
| if ('speechSynthesis' in window && isConversationMode) { | |
| let textToRead = data.response | |
| .replace(/<[^>]+>/g, ' ') // supprime balises HTML | |
| .replace(/[^\w\sàâäçéèêëîïôöùûüÿñœ]/gi, '') // supprime symboles | |
| .replace(/\s+/g, ' ') // normalise espaces | |
| .trim(); | |
| const utterance = new SpeechSynthesisUtterance(textToRead); | |
| utterance.lang = "fr-FR"; | |
| utterance.rate = 1.0; | |
| utterance.pitch = 1.0; | |
| speechSynthesis.cancel(); // stoppe lectures précédentes | |
| speechSynthesis.speak(utterance); | |
| } | |
| convRecognition.start(); // relance automatiquement pour écoute suivante | |
| }) | |
| .catch(error => { | |
| console.error("Erreur conversation:", error); | |
| }); | |
| } | |
| }; | |
| convRecognition.onerror = (event) => { | |
| console.error("Erreur vocale (mode conv):", event.error); | |
| conversationBtn.classList.remove("listening"); | |
| }; | |
| convRecognition.onend = () => { | |
| if (isConversationMode) convRecognition.start(); // boucle tant que mode actif | |
| }; | |
| } else if (conversationBtn) { | |
| conversationBtn.disabled = true; | |
| conversationBtn.title = "Mode conversation vocale non disponible sur ce navigateur"; | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> |