Spaces:
Running
Running
Download index.html from bsalvan/Valdeuck-final: direct link, hf CLI and curl.
- Browser
- Download file 7.63 kB
-
https://huggingface.co/spaces/bsalvan/Valdeuck-final/resolve/main/index.html
- Command line
-
hf download hf://spaces/bsalvan/Valdeuck-final/index.html
-
curl -L -o index.html https://huggingface.co/spaces/bsalvan/Valdeuck-final/resolve/main/index.html
7.63 kB
| <html lang="fr"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> | |
| <title>La Bincherie Chat</title> | |
| <style> | |
| * { box-sizing: border-box; } | |
| body, html { margin: 0; padding: 0; height: 100%; font-family: 'Segoe UI', sans-serif; background: #000; overflow: hidden; } | |
| #chat-window { display: flex; flex-direction: column; height: 100vh; width: 100%; background: #fff; position: fixed; top: 0; left: 0; z-index: 99999; } | |
| #header { background: #1a1a1a; color: #fff; padding: 10px 15px; display: flex; align-items: center; justify-content: space-between; border-bottom: 3px solid #f39c12; flex-shrink: 0; } | |
| .header-brand { display: flex; align-items: center; gap: 12px; } | |
| #header img { width: 50px; height: 50px; border-radius: 50%; border: 2px solid #f39c12; object-fit: cover; } | |
| #messages { flex: 1; overflow-y: auto; padding: 20px; background: #fdfdfd; display: flex; flex-direction: column; gap: 15px; } | |
| .line { display: flex; width: 100%; animation: fadeIn 0.3s ease; } | |
| @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } | |
| .bot-line { justify-content: flex-start; } | |
| .user-line { justify-content: flex-end; } | |
| .bubble { padding: 12px 16px; border-radius: 18px; max-width: 80%; font-size: 15px; line-height: 1.4; } | |
| .bot-bubble { background: #eee; color: #222; border-bottom-left-radius: 2px; } | |
| .user-bubble { background: #f39c12; color: #fff; border-bottom-right-radius: 2px; } | |
| .mini-avatar { width: 35px; height: 35px; border-radius: 50%; margin-right: 8px; align-self: flex-end; border: 1px solid #f39c12; } | |
| #input-area { padding: 15px; background: #fff; border-top: 1px solid #ddd; display: flex; gap: 10px; flex-shrink: 0; align-items: center; } | |
| #q { flex: 1; padding: 12px 18px; border-radius: 25px; border: 1px solid #ccc; outline: none; font-size: 16px; } | |
| .btn { border: none; cursor: pointer; display: flex; align-items: center; justify-content: center; } | |
| #send-btn { background: #f39c12; color: #fff; width: 45px; height: 45px; border-radius: 50%; font-size: 20px; } | |
| #mic-btn { background: #f0f0f0; color: #333; width: 45px; height: 45px; border-radius: 50%; font-size: 20px; border: 1px solid #ccc; } | |
| #mic-btn.active { background: #ff4757; color: #fff; animation: pulse 1s infinite; } | |
| @keyframes pulse { 0% { transform: scale(1); } 50% { transform: scale(1.1); } 100% { transform: scale(1); } } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="chat-window"> | |
| <div id="header"> | |
| <div class="header-brand"> | |
| <img id="main-avatar" src="maitre_deuck.webp"> | |
| <div><strong id="main-name">Maître-Deuck</strong><br><span style="font-size:10px; color:#2ecc71;">● En ligne</span></div> | |
| </div> | |
| <button id="v-tog" onclick="toggleV()" style="background:none; border:none; color:#f39c12; font-size:24px;">🔊</button> | |
| </div> | |
| <div id="messages"></div> | |
| <div id="input-area"> | |
| <button id="mic-btn" class="btn" onclick="startRec()">🎤</button> | |
| <input type="text" id="q" placeholder="Posez votre question..." onkeypress="if(event.key==='Enter') send()"> | |
| <button id="send-btn" class="btn" onclick="send()">🚀</button> | |
| </div> | |
| </div> | |
| <script> | |
| const isMobile = /Android|iPhone|iPad|iPod/i.test(navigator.userAgent); | |
| let chatHistory = []; | |
| const config = { | |
| name: isMobile ? "Margot" : "Maître-Deuck", | |
| avatar: isMobile ? "margot.webp" : "maitre_deuck.webp", | |
| welcome: isMobile ? "Bonjour ! Je suis Margot, l'assistante de Maître-Deuck. Comment puis-je vous aider ? 🍻" : "Bienvenue ! Je suis Maître-Deuck. Qu'est-ce qu'on vous sert aujourd'hui ? 🍻", | |
| pitch: isMobile ? 1.0 : 0.35, | |
| rate: isMobile ? 1.0 : 0.85 | |
| }; | |
| document.getElementById('main-avatar').src = config.avatar; | |
| document.getElementById('main-name').textContent = config.name; | |
| let voiceOn = true; | |
| function toggleV() { voiceOn = !voiceOn; document.getElementById('v-tog').textContent = voiceOn ? "🔊" : "🔇"; } | |
| // --- FONCTION SPEAK UNIQUE ET CORRIGÉE --- | |
| function speak(text) { | |
| if(!voiceOn) return; | |
| window.speechSynthesis.cancel(); | |
| const u = new SpeechSynthesisUtterance(text); | |
| u.lang = 'fr-FR'; | |
| u.pitch = config.pitch; | |
| u.rate = config.rate; | |
| if(!isMobile) { | |
| const voices = window.speechSynthesis.getVoices(); | |
| const maleVoice = voices.find(v => | |
| v.lang.includes('fr') && | |
| /(thomas|nicolas|paul|daniel|google|mâle|male)/i.test(v.name.toLowerCase()) | |
| ); | |
| if(maleVoice) { u.voice = maleVoice; } | |
| } | |
| setTimeout(() => window.speechSynthesis.speak(u), 150); | |
| } | |
| const Rec = window.SpeechRecognition || window.webkitSpeechRecognition; | |
| if(Rec) { | |
| const r = new Rec(); r.lang = 'fr-FR'; | |
| r.onstart = () => document.getElementById('mic-btn').classList.add('active'); | |
| r.onend = () => document.getElementById('mic-btn').classList.remove('active'); | |
| r.onresult = (e) => { document.getElementById('q').value = e.results[0][0].transcript; send(); }; | |
| function startRec() { try { r.start(); } catch(e) { r.stop(); } } | |
| } | |
| async function send() { | |
| const el = document.getElementById('q'); | |
| const val = el.value.trim(); | |
| if(!val) return; | |
| addMsg('user', val); | |
| el.value = ''; | |
| const contextHistory = chatHistory.slice(-4).join("\n"); | |
| const queryWithHistory = contextHistory ? `Historique récent :\n${contextHistory}\n\nNouvelle question : ${val}` : val; | |
| try { | |
| const res = await fetch('/ask', { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({ query: queryWithHistory }) | |
| }); | |
| const d = await res.json(); | |
| addMsg('bot', d.response); | |
| speak(d.response); | |
| chatHistory.push(`Utilisateur: ${val}`); | |
| chatHistory.push(`${config.name}: ${d.response}`); | |
| if(chatHistory.length > 6) chatHistory.shift(); | |
| } catch(e) { addMsg('bot', "Désolé, j'ai une fuite dans mon fût de données... 🍺"); } | |
| } | |
| function addMsg(type, text) { | |
| const container = document.getElementById('messages'); | |
| const line = document.createElement('div'); | |
| line.className = `line ${type}-line`; | |
| line.innerHTML = type === 'bot' | |
| ? `<img src="${config.avatar}" class="mini-avatar"><div class="bubble bot-bubble">${text}</div>` | |
| : `<div class="bubble user-bubble">${text}</div>`; | |
| container.appendChild(line); | |
| container.scrollTop = container.scrollHeight; | |
| } | |
| window.speechSynthesis.onvoiceschanged = () => window.speechSynthesis.getVoices(); | |
| setTimeout(() => { if(document.getElementById('messages').children.length === 0) addMsg('bot', config.welcome); }, 1000); | |
| </script> | |
| </body> | |
| </html> |