| | <!DOCTYPE html> |
| | <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"> |
| | |
| | <aside class="sidebar card"> |
| | <div class="sidebar-header"> |
| | <h2>Les Chats De SeaTech</h2> |
| | <div class="cats-logo"> |
| | <img src="{{ url_for('static', filename='img/franky.png') }}" alt="Franky" class="cat-avatar" |
| | loading="lazy"> |
| | <img src="{{ url_for('static', filename='img/freddy.png') }}" alt="Freddy" class="cat-avatar" |
| | loading="lazy"> |
| | </div> |
| | </div> |
| | <nav> |
| | <div class="sidebar-section"> |
| | <h3>Liens SeaTech</h3> |
| | <ul> |
| | <li><a href="https://seatech.univ-tln.fr/" target="_blank" rel="noopener">SeaTech Officiel</a> |
| | </li> |
| | <li><a href="https://www.univ-tln.fr/" target="_blank" rel="noopener">Université de Toulon</a> |
| | </li> |
| | </ul> |
| | </div> |
| | <div class="sidebar-section"> |
| | <h3>Formations</h3> |
| | <ul> |
| | <li><a href="https://seatech.univ-tln.fr/devenir-ingenieur" target="_blank" |
| | rel="noopener">Devenir Ingénieur</a></li> |
| | <li><a href="https://seatech.univ-tln.fr/Formation-d-ingenieurs-Materiaux-par-apprentissage.html" |
| | target="_blank" rel="noopener">Matériaux (Apprentissage)</a></li> |
| | <li><a href="https://seatech.univ-tln.fr/Formation-d-ingenieurs-en-systemes-numeriques-par-apprentissage.html" |
| | target="_blank" rel="noopener">Systèmes Numériques (Apprentissage)</a></li> |
| | </ul> |
| | </div> |
| | <div class="sidebar-section"> |
| | <h3>Informations</h3> |
| | <ul> |
| | <li><a href="https://seatech.univ-tln.fr/recherche" target="_blank" rel="noopener">Recherche</a> |
| | </li> |
| | <li><a href="https://seatech.univ-tln.fr/international" target="_blank" |
| | rel="noopener">International</a></li> |
| | <li><a href="https://seatech.univ-tln.fr/Contacts.html" target="_blank" |
| | rel="noopener">Contacts</a></li> |
| | </ul> |
| | </div> |
| | </nav> |
| | <div class="sidebar-footer"> |
| | <p>Posez-moi vos questions sur SeaTech!</p> |
| | </div> |
| | </aside> |
| |
|
| | |
| | <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> |
| |
|
| | |
| | {% 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"> |
| | <p>Rôle sélectionné : <strong id="selectedRoleName"></strong></p> |
| | <button class="confirm-button" id="confirmRoleBtn"> |
| | <i class="fas fa-check"></i> Confirmer mon profil |
| | </button> |
| | </div> |
| | </section> |
| | {% else %} |
| | |
| | <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> |
| | |
| | <button type="button" id="micButton" class="mic-button" {% if not user_profile.get('confirmed') |
| | %}disabled{% endif %}> |
| | <i class="fas fa-microphone"></i> |
| | </button> |
| |
|
| | |
| | <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> |
| |
|
| | |
| | <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"> |
| | |
| | </div> |
| | </aside> |
| |
|
| | |
| | <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; |
| | |
| | |
| | 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 roleButtons = document.querySelectorAll('.role-button'); |
| | const confirmRoleSection = document.querySelector('.confirm-role-section'); |
| | const confirmRoleBtn = document.getElementById('confirmRoleBtn'); |
| | const resetRoleBtn = document.getElementById('resetRoleBtn'); |
| | const selectedRoleName = document.getElementById('selectedRoleName'); |
| | |
| | let selectedRole = null; |
| | 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!" |
| | ]; |
| | |
| | |
| | roleButtons.forEach(button => { |
| | button.addEventListener('click', function () { |
| | roleButtons.forEach(btn => btn.classList.remove('selected')); |
| | this.classList.add('selected'); |
| | selectedRole = this.dataset.role; |
| | selectedRoleName.textContent = selectedRole; |
| | confirmRoleSection.classList.add('active'); |
| | }); |
| | }); |
| | |
| | if (confirmRoleBtn) { |
| | confirmRoleBtn.addEventListener('click', function () { |
| | if (selectedRole) { |
| | console.log('Envoi de la sélection de rôle:', selectedRole); |
| | confirmRoleBtn.disabled = true; |
| | confirmRoleBtn.textContent = 'En cours...'; |
| | localStorage.setItem("seatech_role", selectedRole); |
| | fetch('/api/ask', { |
| | method: 'POST', |
| | headers: { 'Content-Type': 'application/json' }, |
| | body: JSON.stringify({ role_selection: selectedRole }), |
| | credentials: 'include' |
| | }) |
| | .then(response => { |
| | console.log('Réponse status:', response.status); |
| | return response.json(); |
| | }) |
| | .then(data => { |
| | 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); |
| | } else { |
| | console.error('Erreur: statut inattendu', data); |
| | alert('Erreur lors de la sélection du rôle. Veuillez réessayer.'); |
| | confirmRoleBtn.disabled = false; |
| | confirmRoleBtn.textContent = '✓ Confirmer mon profil'; |
| | } |
| | }) |
| | .catch(error => { |
| | console.error('Erreur lors de la sélection de rôle:', error); |
| | alert('Erreur réseau: ' + error.message); |
| | confirmRoleBtn.disabled = false; |
| | confirmRoleBtn.textContent = '✓ Confirmer mon profil'; |
| | }); |
| | } |
| | }); |
| | |
| | |
| | |
| | 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"); |
| | |
| | fetch('/api/ask', { |
| | method: 'POST', |
| | headers: { 'Content-Type': 'application/json' }, |
| | body: JSON.stringify({ role_selection: selectedRole }), |
| | credentials: 'include' |
| | }) |
| | .then(resp => { |
| | if (resp.status === 429) { |
| | console.log('Trop de requêtes, réessai dans 5 secondes...'); |
| | sessionStorage.removeItem("auto_submit_attempted"); |
| | setTimeout(() => { |
| | window.location.reload(); |
| | }, 5000); |
| | return; |
| | } |
| | return resp.json(); |
| | }) |
| | .then(data => { |
| | if (data && data.status === 'role_selected') { |
| | console.log('Auto-enregistrement rôle réussi'); |
| | sessionStorage.setItem('scroll_to_chat', '1'); |
| | window.location.href = window.location.origin + '/'; |
| | } |
| | }) |
| | .catch(err => { |
| | console.error('Erreur auto-role:', err); |
| | sessionStorage.removeItem("auto_submit_attempted"); |
| | }); |
| | } |
| | } |
| | |
| | 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(); |
| | |
| | |
| | |
| | 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); |
| | } |
| | |
| | |
| | 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(); |
| | }); |
| | }); |
| | } |
| | |
| | |
| | const micBtn = document.getElementById('micButton'); |
| | if (micBtn && 'webkitSpeechRecognition' in window) { |
| | const recognition = new webkitSpeechRecognition(); |
| | recognition.lang = "fr-FR"; |
| | recognition.continuous = false; |
| | recognition.interimResults = false; |
| | |
| | micBtn.addEventListener('click', () => { |
| | recognition.start(); |
| | micBtn.classList.add("listening"); |
| | }); |
| | |
| | recognition.onresult = (event) => { |
| | let transcript = event.results[0][0].transcript; |
| | |
| | |
| | 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"); |
| | transcript = transcript.replace(regex, correct); |
| | } |
| | |
| | queryInput.value = transcript; |
| | |
| | micBtn.classList.remove("listening"); |
| | }; |
| | |
| | |
| | recognition.onerror = (event) => { |
| | console.error("Erreur vocale:", event.error); |
| | micBtn.classList.remove("listening"); |
| | }; |
| | |
| | recognition.onend = () => { |
| | micBtn.classList.remove("listening"); |
| | }; |
| | } |
| | |
| | |
| | |
| | |
| | const conversationBtn = document.getElementById("conversationBtn"); |
| | if (conversationBtn && 'webkitSpeechRecognition' in window) { |
| | const convRecognition = new webkitSpeechRecognition(); |
| | convRecognition.lang = "fr-FR"; |
| | convRecognition.continuous = false; |
| | convRecognition.interimResults = false; |
| | |
| | |
| | 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(); |
| | } |
| | }); |
| | |
| | convRecognition.onresult = (event) => { |
| | let transcript = event.results[0][0].transcript; |
| | |
| | |
| | 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); |
| | } |
| | |
| | |
| | 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(); |
| | |
| | |
| | if ('speechSynthesis' in window && isConversationMode) { |
| | 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); |
| | } |
| | |
| | convRecognition.start(); |
| | }) |
| | .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(); |
| | }; |
| | } |
| | |
| | }); |
| | </script> |
| | </body> |
| |
|
| | </html> |