AnthonyHerve56's picture
Ajotu smooth scroll
9ad55f7
<!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">
<!-- Sidebar -->
<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>
<!-- 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">
<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 %}
<!-- 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>&copy; {{ 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é
// 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');
// Éléments de sélection de rôle
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!"
];
// Gestion de la sélection de rôle
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');
// Attendre 500ms avant de recharger pour assurer la sauvegarde de la session
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';
});
}
});
// 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");
// lancer en tâche de fond, sans bloquer
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"); // permettre un nouvel essai
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"); // permettre un nouvel essai en cas d'erreur
});
}
}
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 && '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"); // 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");
};
}
// === Mode conversation ===
const conversationBtn = document.getElementById("conversationBtn");
if (conversationBtn && 'webkitSpeechRecognition' in window) {
const convRecognition = new webkitSpeechRecognition();
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
};
}
});
</script>
</body>
</html>