Files
ChatBeta/index.html
2026-01-20 13:28:07 +01:00

319 lines
11 KiB
HTML

<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chatβeta*</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css">
<link rel="icon" type="image/svg+xml" href="cat.svg">
<link rel="apple-touch-icon" sizes="240x240" href="thumbnail.png" />
<meta name="apple-mobile-web-app-title" content="Chatβeta*" />
<meta property="og:title" content="😸 Chatβeta*" />
<meta property="og:type" content="game" />
<meta property="og:locale" content="fr_FR" />
<style>
body {
--vph: 100vh;
display: flex;
flex-direction: column;
min-height: var(--vph);
}
@media screen and (min-height: 600px) {
body {
height: var(--vph);
}
body > header {
position: sticky;
top: 0;
z-index: 10;
width: 100%;
background: linear-gradient(var(--pico-background-color) 60%, transparent);
text-shadow: 0 0 10px var(--pico-background-color);
}
body > main {
margin: -5rem;
padding: 5rem;
}
body > footer {
position: sticky;
bottom: 0;
z-index: 10;
background: var(--pico-background-color);
}
}
.container {
max-width: 700px;
}
body > header .container {
max-width: 750px;
}
body > main {
flex-grow: 2;
}
.reponse::before {
content: "😸 ";
}
#bouton_reconnaissance_vocale {
position: absolute;
right: 0;
width: 2rem;
height: 100%;
padding-left: 0;
border: none;
z-index: 10;
}
#bouton_envoyer {
padding: 1rem;
}
@media print {
#bouton_synthese_vocale,
#formulaire {
display: none !important;
}
}
</style>
</head>
<body>
<header>
<nav class="container">
<h1>Chat<small><em>βeta</em><span style="color: #9B2318">*</span></small></h1>
<button id="bouton_synthese_vocale" type="button" class="secondary" style="display: none;" disabled><i class="bi bi-volume-off-fill"></i></button>
</nav>
</header>
<main class="container" id="conversation">
<p class="reponse">Posez-moi toutes vos questions !</p>
</main>
<footer class="container">
<form id="formulaire" action="question.php" method="post" role="group">
<fieldset role="group">
<textarea id="question" name="question" placeholder="Ma question" required></textarea>
<button id="bouton_reconnaissance_vocale" type="button" class="outline secondary" style="display: none;"><i class="bi bi-mic-fill"></i></button>
</fieldset>
<button id="bouton_envoyer" type="submit"><i class="bi bi-send-fill"></i></button>
</form>
</footer>
<dialog id="boite_synthese_vocale">
<article>
<header>
<button id="bouton_fermer" aria-label="Close" rel="prev"></button>
<p>
<strong><i class="bi bi-volume-up-fill"></i> Synthèse vocale</strong>
</p>
</header>
<form>
<fieldset>
<label for="select_voix">Voix disponibles :</label>
<select id="select_voix">
<option value="" selected>Pas de synthèse vocale</option>
</select>
</fieldset>
</form>
<footer>
<button id="bouton_annuler" class="secondary">Annuler</button>
<button id="bouton_ok">OK</button>
</footer>
</article>
</dialog>
<script>
const formulaire = document.getElementById('formulaire');
const bouton_envoyer = document.getElementById('bouton_envoyer');
const conversation = document.getElementById('conversation');
const question = document.getElementById('question');
const bouton_synthese_vocale = document.getElementById('bouton_synthese_vocale');
const boite_synthese_vocale = document.getElementById('boite_synthese_vocale');
const select_voix = document.getElementById('select_voix');
const aphone = select_voix.innerHTML;
const bouton_fermer = document.getElementById('bouton_fermer');
const bouton_annuler = document.getElementById('bouton_annuler');
const bouton_ok = document.getElementById('bouton_ok');
const footer = document.querySelector('footer');
const langue = document.documentElement.lang;
let voix_selectionnee = null;
let utterance = null;
question.onkeydown = function(e) {
if (e.key === 'Enter' && !e.ctrlKey && !e.shiftKey) {
e.preventDefault();
e.target.form.requestSubmit();
}
};
question.onfocus = function() {
window.onresize;
question.scrollIntoView({ block: 'start', behavior: 'auto' });
};
formulaire.addEventListener('submit', async (e) => {
e.preventDefault();
if (bouton_envoyer.disabled == true) return;
bouton_envoyer.disabled = true;
let bouton_envoyer_innerHTML = bouton_envoyer.innerHTML;
bouton_envoyer.innerHTML = "";
bouton_envoyer.setAttribute("aria-busy", true)
const formulaireData = new FormData(formulaire);
const citation = document.createElement('article');
citation.innerText = formulaireData.get('question');
conversation.appendChild(citation);
formulaire.reset();
const paragraphe = document.createElement('p');
paragraphe.setAttribute("aria-busy", "true");
conversation.appendChild(paragraphe);
conversation.scrollTop = conversation.scrollHeight;
const requete = await fetch(formulaire.action, {
method: formulaire.method,
body: formulaireData
});
paragraphe.setAttribute('aria-busy', 'true');
const reponse = await requete.text();
paragraphe.setAttribute('aria-busy', 'false');
if (voix_selectionnee) {
utterance.text = reponse;
speechSynthesis.speak(utterance);
}
let t = 0;
Array.from(reponse).forEach((lettre, i) => {
setTimeout(() => {
if (lettre == "\n") {
paragraphe.innerHTML += "<br>";
} else {
paragraphe.innerHTML += lettre;
}
paragraphe.scrollIntoView({ block: 'start', behavior: 'auto' });
}, t += 100 * Math.random());
});
setTimeout(() => {
const paragraphe = document.createElement('p');
paragraphe.classList.add('reponse');
paragraphe.innerHTML = 'Voulez-vous que je réponde à une autre question ?';
conversation.appendChild(paragraphe);
paragraphe.scrollIntoView({ block: 'start', behavior: 'auto' });
bouton_envoyer.disabled = false;
bouton_envoyer.setAttribute("aria-busy", false);
bouton_envoyer.innerHTML = bouton_envoyer_innerHTML;
question.focus();
question.onfocus();
}, t);
})
// Synthèse vocale
function charger_voix() {
let liste_voix = speechSynthesis.getVoices(); // .filter(voice => voice.lang.startsWith(langue));
liste_voix.sort((a, b) => b.lang.startsWith(langue));
liste_voix.sort((a, b) => b.default);
select_voix.innerHTML = aphone;
if (!liste_voix.length) {
bouton_synthese_vocale.disabled = true;
speechSynthesis.addEventListener('voiceschanged', charger_voix);
return;
}
speechSynthesis.removeEventListener('voiceschanged', charger_voix);
liste_voix.forEach((voix, i) => {
const option = document.createElement('option');
option.value = i;
option.textContent = voix.name;
select_voix.appendChild(option);
if (voix.voiceURI === voix_selectionnee) {
option.selected = true;
select_voix.value = i;
utterance.voice = voix;
bouton_synthese_vocale.innerHTML = `<i class="bi bi-volume-up-fill"></i>`;
}
})
bouton_fermer.onclick = bouton_annuler.onclick = function() {
boite_synthese_vocale.close();
};
bouton_ok.onclick = function() {
boite_synthese_vocale.close();
if (select_voix.value) {
let voix = liste_voix[select_voix.value];
utterance.voice = voix;
voix_selectionnee = voix.voiceURI;
window.localStorage.setItem('voiceURI', voix.voiceURI);
bouton_synthese_vocale.innerHTML = `<i class="bi bi-volume-up-fill"></i>`;
} else {
voix_selectionnee = null;
window.localStorage.removeItem('voiceURI');
bouton_synthese_vocale.innerHTML = `<i class="bi bi-volume-off-fill"></i>`;
}
};
bouton_synthese_vocale.onclick = function() {
boite_synthese_vocale.showModal();
};
bouton_synthese_vocale.disabled = false;
}
if ('speechSynthesis' in window) {
voix_selectionnee = window.localStorage.getItem('voiceURI');
utterance = new SpeechSynthesisUtterance();
utterance.lang = langue;
utterance.rate = 1;
charger_voix();
bouton_synthese_vocale.style.display = 'block';
}
// Reconnaissance vocale
var SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
if (SpeechRecognition) {
bouton_reconnaissance_vocale.addEventListener('click', () => {
const reconnaissance= new SpeechRecognition();
reconnaissance.lang = langue;
reconnaissance.continuous = false;
reconnaissance.interimResults = false;
reconnaissance.maxAlternatives = 1;
bouton_reconnaissance_vocale_inner_HTML = bouton_reconnaissance_vocale.innerHTML
bouton_reconnaissance_vocale.innerHTML = ""
bouton_reconnaissance_vocale.setAttribute("aria-busy", true)
reconnaissance.onresult = function(event) {
const transcript = event.results[0][0].transcript;
question.value = transcript;
if (!bouton_envoyer.disabled) formulaire.requestSubmit()
};
reconnaissance.onspeechend =
reconnaissance.onerror =
reconnaissance.onnomatch = function () {
reconnaissance.stop();
bouton_reconnaissance_vocale.setAttribute("aria-busy", false)
bouton_reconnaissance_vocale.innerHTML = bouton_reconnaissance_vocale_inner_HTML
};
reconnaissance.start();
});
bouton_reconnaissance_vocale.style.display = 'block';
}
// Adaptation au clavier virtuel du téléphone
window.onresize = function() {
document.body.style.setProperty("--vph", `${window.visualViewport.height}px`);
}
window.visualViewport?.addEventListener('resize', window.onresize);
window.onresize();
</script>
</body>
</html>