Spaces:
Running
Running
| <html lang="ar" dir="rtl"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>مساعد صوتي متحرك - وضع ليلي/نهاري</title> | |
| <style> | |
| * { margin:0; padding:0; box-sizing:border-box; font-family:'Segoe UI','Noto Sans Arabic',sans-serif; } | |
| body { | |
| background:#fff; | |
| display:flex; | |
| justify-content:center; | |
| align-items:center; | |
| min-height:100vh; | |
| flex-direction:column; | |
| color: rgb(11, 186, 131); | |
| transition: all 0.3s ease; | |
| overflow: hidden; | |
| position: relative; | |
| } | |
| /* تنسيقات الوضع الليلي */ | |
| body.night-mode { | |
| background: #1a202c; | |
| color: rgb(11, 186, 131); | |
| } | |
| /* تأثير خلفية ديناميكي */ | |
| .background-effect { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| z-index: -1; | |
| opacity: 0.1; | |
| background: radial-gradient(circle at 20% 50%, rgba(11, 186, 131, 0.4) 0%, transparent 50%), | |
| radial-gradient(circle at 80% 80%, rgba(11, 186, 131, 0.3) 0%, transparent 40%); | |
| animation: backgroundMove 20s infinite alternate ease-in-out; | |
| } | |
| @keyframes backgroundMove { | |
| 0% { transform: scale(1) rotate(0deg); } | |
| 100% { transform: scale(1.2) rotate(5deg); } | |
| } | |
| .night-mode .background-effect { | |
| background: radial-gradient(circle at 20% 50%, rgba(11, 186, 131, 0.2) 0%, transparent 50%), | |
| radial-gradient(circle at 80% 80%, rgba(11, 186, 131, 0.15) 0%, transparent 40%); | |
| } | |
| .circle-outer { | |
| width:220px; height:220px; | |
| border-radius:50%; | |
| background: rgba(11, 186, 131, 0.15); | |
| display:flex; justify-content:center; align-items:center; | |
| cursor:pointer; transition: all 0.3s ease; | |
| box-shadow: 0 15px 30px rgba(0,0,0,0.1); | |
| position: relative; | |
| overflow:hidden; | |
| animation: subtlePulse 3s infinite ease-in-out; | |
| z-index: 10; | |
| } | |
| @keyframes subtlePulse { | |
| 0% { transform: scale(1); box-shadow: 0 15px 30px rgba(0,0,0,0.1); } | |
| 50% { transform: scale(1.02); box-shadow: 0 20px 40px rgba(0,0,0,0.15); } | |
| 100% { transform: scale(1); box-shadow: 0 15px 30px rgba(0,0,0,0.1); } | |
| } | |
| .night-mode .circle-outer { | |
| background: rgba(11, 186, 131, 0.15); | |
| box-shadow: 0 15px 30px rgba(0,0,0,0.3); | |
| animation: subtlePulseNight 3s infinite ease-in-out; | |
| } | |
| @keyframes subtlePulseNight { | |
| 0% { transform: scale(1); box-shadow: 0 15px 30px rgba(0,0,0,0.3); } | |
| 50% { transform: scale(1.02); box-shadow: 0 20px 40px rgba(0,0,0,0.4); } | |
| 100% { transform: scale(1); box-shadow: 0 15px 30px rgba(0,0,0,0.3); } | |
| } | |
| .circle-middle { | |
| width:180px; height:180px; | |
| border-radius:50%; | |
| background: rgba(11, 186, 131, 0.25); | |
| display:flex; justify-content:center; align-items:center; | |
| transition: all 0.3s ease; | |
| animation: middlePulse 4s infinite ease-in-out; | |
| } | |
| @keyframes middlePulse { | |
| 0% { transform: scale(1); background: rgba(11, 186, 131, 0.25); } | |
| 50% { transform: scale(1.03); background: rgba(11, 186, 131, 0.3); } | |
| 100% { transform: scale(1); background: rgba(11, 186, 131, 0.25); } | |
| } | |
| .night-mode .circle-middle { | |
| background: rgba(11, 186, 131, 0.25); | |
| animation: middlePulseNight 4s infinite ease-in-out; | |
| } | |
| @keyframes middlePulseNight { | |
| 0% { transform: scale(1); background: rgba(11, 186, 131, 0.25); } | |
| 50% { transform: scale(1.03); background: rgba(11, 186, 131, 0.3); } | |
| 100% { transform: scale(1); background: rgba(11, 186, 131, 0.25); } | |
| } | |
| .circle-inner { | |
| width:140px; height:140px; | |
| border-radius:50%; | |
| background:white; | |
| display:flex; justify-content:center; align-items:center; | |
| box-shadow: 0 5px 15px rgba(0,0,0,0.1); | |
| transition: all 0.3s ease; | |
| animation: innerGlow 5s infinite alternate; | |
| } | |
| @keyframes innerGlow { | |
| 0% { box-shadow: 0 5px 15px rgba(0,0,0,0.1); } | |
| 100% { box-shadow: 0 5px 25px rgba(11, 186, 131, 0.3); } | |
| } | |
| .night-mode .circle-inner { | |
| background: #2d3748; | |
| box-shadow: 0 5px 15px rgba(0,0,0,0.3); | |
| animation: innerGlowNight 5s infinite alternate; | |
| } | |
| @keyframes innerGlowNight { | |
| 0% { box-shadow: 0 5px 15px rgba(0,0,0,0.3); } | |
| 100% { box-shadow: 0 5px 25px rgba(11, 186, 131, 0.4); } | |
| } | |
| .mic-svg { | |
| width:60px; height:60px; | |
| fill: rgb(11, 186, 131); | |
| transition: all 0.3s ease; | |
| animation: iconFloat 6s infinite ease-in-out; | |
| } | |
| @keyframes iconFloat { | |
| 0% { transform: translateY(0) scale(1); } | |
| 50% { transform: translateY(-5px) scale(1.05); } | |
| 100% { transform: translateY(0) scale(1); } | |
| } | |
| .night-mode .mic-svg { | |
| fill: rgb(11, 186, 131); | |
| } | |
| /* أثناء الاستماع */ | |
| .circle-outer.listening { | |
| animation: rotatePulse 1.5s infinite linear, colorShift 3s infinite alternate; | |
| } | |
| @keyframes rotatePulse { | |
| 0% { transform: rotate(0deg) scale(1); } | |
| 25% { transform: rotate(5deg) scale(1.05); } | |
| 50% { transform: rotate(0deg) scale(1.1); } | |
| 75% { transform: rotate(-5deg) scale(1.05); } | |
| 100% { transform: rotate(0deg) scale(1); } | |
| } | |
| @keyframes colorShift { | |
| 0% { background: rgba(11, 186, 131, 0.15); } | |
| 25% { background: rgba(11, 186, 131, 0.25); } | |
| 50% { background: rgba(11, 186, 131, 0.35); } | |
| 75% { background: rgba(11, 186, 131, 0.25); } | |
| 100% { background: rgba(11, 186, 131, 0.15); } | |
| } | |
| .night-mode .circle-outer.listening { | |
| animation: rotatePulse 1.5s infinite linear, colorShiftNight 3s infinite alternate; | |
| } | |
| @keyframes colorShiftNight { | |
| 0% { background: rgba(11, 186, 131, 0.15); } | |
| 25% { background: rgba(11, 186, 131, 0.25); } | |
| 50% { background: rgba(11, 186, 131, 0.35); } | |
| 75% { background: rgba(11, 186, 131, 0.25); } | |
| 100% { background: rgba(11, 186, 131, 0.15); } | |
| } | |
| /* حركة الميكروفون أثناء الرد */ | |
| .circle-outer.speaking .mic-svg { | |
| animation: speakIcon 0.8s infinite alternate, glowMic 1.2s infinite alternate; | |
| } | |
| @keyframes speakIcon { | |
| 0% { transform: translateY(0) scale(1); } | |
| 25% { transform: translateY(-8px) scale(1.1); } | |
| 50% { transform: translateY(0) scale(1); } | |
| 75% { transform: translateY(8px) scale(1.1); } | |
| 100% { transform: translateY(0) scale(1); } | |
| } | |
| @keyframes glowMic { | |
| 0% { filter: drop-shadow(0 0 0 rgba(11, 186, 131, 0.4)); } | |
| 50% { filter: drop-shadow(0 0 15px rgba(11, 186, 131, 0.8)); } | |
| 100% { filter: drop-shadow(0 0 0 rgba(11, 186, 131, 0.4)); } | |
| } | |
| .night-mode .circle-outer.speaking .mic-svg { | |
| animation: speakIcon 0.8s infinite alternate, glowMicNight 1.2s infinite alternate; | |
| } | |
| @keyframes glowMicNight { | |
| 0% { filter: drop-shadow(0 0 0 rgba(11, 186, 131, 0.4)); } | |
| 50% { filter: drop-shadow(0 0 15px rgba(11, 186, 131, 0.8)); } | |
| 100% { filter: drop-shadow(0 0 0 rgba(11, 186, 131, 0.4)); } | |
| } | |
| /* موجات نابضة حول الدائرة - دائمة الآن */ | |
| .pulse-wave { | |
| position:absolute; | |
| border:2px solid rgba(11, 186, 131, 0.5); | |
| border-radius:50%; | |
| width:220px; | |
| height:220px; | |
| top:0; left:0; | |
| animation: pulse 3s infinite ease-in-out; | |
| opacity: 0.7; | |
| } | |
| .pulse-wave:nth-child(2) { | |
| animation-delay: 1s; | |
| } | |
| .pulse-wave:nth-child(3) { | |
| animation-delay: 2s; | |
| } | |
| @keyframes pulse { | |
| 0% { transform: scale(1); opacity:0.5; } | |
| 50% { transform: scale(1.3); opacity:0.2; } | |
| 100% { transform: scale(1.6); opacity:0; } | |
| } | |
| .night-mode .pulse-wave { | |
| border:2px solid rgba(11, 186, 131, 0.5); | |
| } | |
| /* أشرطة الصوت */ | |
| .wave { display:flex; justify-content:center; align-items:flex-end; height:50px; margin-top:15px; } | |
| .wave span { | |
| display:inline-block; | |
| width:5px; height:25px; | |
| background: rgb(11, 186, 131); | |
| margin:0 4px; border-radius:3px; | |
| animation: wave 1.2s infinite ease-in-out; | |
| } | |
| .wave span:nth-child(2){animation-delay:0.1s;} | |
| .wave span:nth-child(3){animation-delay:0.2s;} | |
| .wave span:nth-child(4){animation-delay:0.3s;} | |
| .wave span:nth-child(5){animation-delay:0.4s;} | |
| @keyframes wave { | |
| 0%,40%,100% { transform: scaleY(0.6); } | |
| 20% { transform: scaleY(1.5); } | |
| } | |
| .night-mode .wave span { | |
| background: rgb(11, 186, 131); | |
| } | |
| .hidden { display:none; } | |
| .status { | |
| text-align:center; | |
| margin-top:20px; | |
| font-size:18px; | |
| color:#4a5568; | |
| transition: all 0.3s ease; | |
| min-height: 30px; | |
| padding: 0 10px; | |
| } | |
| .night-mode .status { | |
| color: #a0aec0; | |
| } | |
| /* زر تبديل الوضع */ | |
| .theme-toggle { | |
| position: absolute; | |
| top: 20px; | |
| right: 20px; | |
| background: rgba(11, 186, 131, 0.1); | |
| border: none; | |
| border-radius: 50%; | |
| width: 50px; | |
| height: 50px; | |
| cursor: pointer; | |
| font-size: 24px; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| transition: all 0.3s ease; | |
| box-shadow: 0 2px 10px rgba(0,0,0,0.1); | |
| animation: buttonPulse 4s infinite ease-in-out; | |
| z-index: 100; | |
| } | |
| @keyframes buttonPulse { | |
| 0% { transform: scale(1); box-shadow: 0 2px 10px rgba(0,0,0,0.1); } | |
| 50% { transform: scale(1.05); box-shadow: 0 4px 15px rgba(0,0,0,0.2); } | |
| 100% { transform: scale(1); box-shadow: 0 2px 10px rgba(0,0,0,0.1); } | |
| } | |
| .night-mode .theme-toggle { | |
| background: rgba(11, 186, 131, 0.1); | |
| box-shadow: 0 2px 10px rgba(0,0,0,0.3); | |
| } | |
| .theme-toggle:hover { | |
| transform: scale(1.1); | |
| animation: none; | |
| } | |
| /* رسالة حالة المتصفح */ | |
| .browser-warning { | |
| position: fixed; | |
| bottom: 20px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| background: #ff4757; | |
| color: white; | |
| padding: 10px 20px; | |
| border-radius: 5px; | |
| font-size: 14px; | |
| display: none; | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.1); | |
| z-index: 1000; | |
| text-align: center; | |
| max-width: 90%; | |
| } | |
| /* تأثيرات النص المتحركة */ | |
| @keyframes textPulse { | |
| 0% { opacity: 0.8; } | |
| 50% { opacity: 1; } | |
| 100% { opacity: 0.8; } | |
| } | |
| .listening-text { | |
| animation: textPulse 1.5s infinite; | |
| font-weight: bold; | |
| } | |
| /* تخصيص شريط التمرير */ | |
| ::-webkit-scrollbar { | |
| width: 8px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: rgba(11, 186, 131, 0.1); | |
| border-radius: 10px; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: rgba(11, 186, 131, 0.3); | |
| border-radius: 10px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: rgba(11, 186, 131, 0.5); | |
| } | |
| .night-mode ::-webkit-scrollbar-track { | |
| background: rgba(11, 186, 131, 0.05); | |
| } | |
| .night-mode ::-webkit-scrollbar-thumb { | |
| background: rgba(11, 186, 131, 0.2); | |
| } | |
| .night-mode ::-webkit-scrollbar-thumb:hover { | |
| background: rgba(11, 186, 131, 0.4); | |
| } | |
| /* لوحة الإعدادات */ | |
| .settings-panel { | |
| position: absolute; | |
| top: 80px; | |
| right: 20px; | |
| background: white; | |
| border-radius: 10px; | |
| padding: 15px; | |
| box-shadow: 0 5px 20px rgba(0,0,0,0.1); | |
| display: none; | |
| z-index: 100; | |
| min-width: 200px; | |
| } | |
| .night-mode .settings-panel { | |
| background: #2d3748; | |
| box-shadow: 0 5px 20px rgba(0,0,0,0.3); | |
| } | |
| .settings-panel.show { | |
| display: block; | |
| } | |
| .setting-item { | |
| margin: 10px 0; | |
| text-align: right; | |
| } | |
| .setting-item label { | |
| display: block; | |
| margin-bottom: 5px; | |
| color: #4a5568; | |
| font-size: 14px; | |
| } | |
| .night-mode .setting-item label { | |
| color: #a0aec0; | |
| } | |
| .setting-item select, .setting-item input[type=range] { | |
| width: 100%; | |
| padding: 5px; | |
| border-radius: 5px; | |
| border: 1px solid #e2e8f0; | |
| } | |
| .night-mode .setting-item select, | |
| .night-mode .setting-item input[type=range] { | |
| background: #4a5568; | |
| border-color: #718096; | |
| color: white; | |
| } | |
| .speed-value { | |
| display: inline-block; | |
| margin-right: 10px; | |
| font-size: 14px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- تأثير الخلفية الديناميكي --> | |
| <div class="background-effect"></div> | |
| <!-- زر تبديل الوضع --> | |
| <button class="theme-toggle" id="themeToggle">🌙</button> | |
| <!-- لوحة الإعدادات --> | |
| <div class="settings-panel" id="settingsPanel"> | |
| <div class="setting-item"> | |
| <label for="voiceSelect">نوع الصوت:</label> | |
| <select id="voiceSelect"> | |
| <option value="alloy">alloy</option> | |
| </select> | |
| </div> | |
| <div class="setting-item"> | |
| <label for="speedRange">سرعة الكلام:</label> | |
| <input type="range" id="speedRange" min="0.5" max="2.0" step="0.1" value="1"> | |
| <span class="speed-value" id="speedValue">1.0</span> | |
| </div> | |
| </div> | |
| <div class="circle-outer" id="micCircle"> | |
| <div class="pulse-wave"></div> | |
| <div class="pulse-wave"></div> | |
| <div class="pulse-wave"></div> | |
| <div class="circle-middle"> | |
| <div class="circle-inner"> | |
| <svg class="mic-svg" focusable="false" viewBox="0 0 24 24" aria-hidden="true" role="img"> | |
| <title>API</title> | |
| <path d="M0 0h24v24H0z" fill="none"></path> | |
| <path d="M6 13c-.55 0-1 .45-1 1s.45 1 1 1 1-.45 1-1-.45-1-1-1zm0 4c-.55 0-1 .45-1 1s.45 1 1 1 1-.45 1-1-.45-1-1-1zm0-8c-.55 0-1 .45-1 1s.45 1 1 1 1-.45 1-1-.45-1-1-1zm-3 .5c-.28 0-.5.22-.5.5s.22.5.5.5.5-.22.5-.5-.22-.5-.5-.5zM6 5c-.55 0-1 .45-1 1s.45 1 1 1 1-.45 1-1-.45-1-1-1zm15 5.5c.28 0 .5-.22.5-.5s-.22-.5-.5-.5-.5.22-.5.5.22.5.5.5zM14 7c.55 0 1-.45 1-1s-.45-1-1-1-1 .45-1 1 .45 1 1 1zm0-3.5c.28 0 .5-.22.5-.5s-.22-.5-.5-.5-.5.22-.5.5.22.5.5.5zm-11 10c-.28 0-.5.22-.5.5s.22.5.5.5.5-.22.5-.5-.22-.5-.5-.5zm7 7c-.28 0-.5.22-.5.5s.22.5.5.5.5-.22.5-.5-.22-.5-.5-.5zm0-17c.28 0 .5-.22.5-.5s-.22-.5-.5-.5-.5.22-.5.5.22.5.5.5zM10 7c.55 0 1-.45 1-1s-.45-1-1-1-1 .45-1 1 .45 1 1 1zm0 5.5c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zm8 .5c-.55 0-1 .45-1 1s.45 1 1 1 1-.45 1-1-.45-1-1-1zm0 4c-.55 0-1 .45-1 1s.45 1 1 1 1-.45 1-1-.45-1-1-1zm0-8c-.55 0-1 .45-1 1s.45 1 1 1 1-.45 1-1-.45-1-1-1zm0-4c-.55 0-1 .45-1 1s.45 1 1 1 1-.45 1-1-.45-1-1-1zm3 8.5c-.28 0-.5.22-.5.5s.22.5.5.5.5-.22.5-.5-.22-.5-.5-.5zM14 17c-.55 0-1 .45-1 1s.45 1 1 1 1-.45 1-1-.45-1-1-1zm0 3.5c-.28 0-.5.22-.5.5s.22.5.5.5.5-.22.5-.5-.22-.5-.5-.5zm-4-12c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zm0 8.5c-.55 0-1 .45-1 1s.45 1 1 1 1-.45 1-1-.45-1-1-1zm4-4.5c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zm0-4c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5z"></path> | |
| </svg> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="status" id="status">انقر على الدائرة للتحدث</div> | |
| <div class="wave hidden" id="wave"> | |
| <span></span><span></span><span></span><span></span><span></span> | |
| </div> | |
| <div class="browser-warning" id="browserWarning"> | |
| عذرًا، متصفحك الحالي لا يدعم خاصية التعرف على الصوت. يرجى استخدام Chrome أو Edge للحصول على أفضل تجربة. | |
| </div> | |
| <script> | |
| const micCircle = document.getElementById('micCircle'); | |
| const status = document.getElementById('status'); | |
| const wave = document.getElementById('wave'); | |
| const themeToggle = document.getElementById('themeToggle'); | |
| const browserWarning = document.getElementById('browserWarning'); | |
| const settingsPanel = document.getElementById('settingsPanel'); | |
| const speedRange = document.getElementById('speedRange'); | |
| const speedValue = document.getElementById('speedValue'); | |
| // تحديث عرض سرعة الصوت | |
| speedRange.addEventListener('input', () => { | |
| speedValue.textContent = speedRange.value; | |
| }); | |
| // التحقق من دعم المتصفح للوضع الليلي | |
| let isNightMode = localStorage.getItem('nightMode') === 'true'; | |
| // تطبيق الوضع الليلي عند التحميل | |
| if (isNightMode) { | |
| document.body.classList.add('night-mode'); | |
| themeToggle.textContent = '☀️'; | |
| } | |
| // تبديل الوضع الليلي/النهاري | |
| themeToggle.addEventListener('click', function () { | |
| isNightMode = !isNightMode; | |
| document.body.classList.toggle('night-mode'); | |
| if (isNightMode) { | |
| themeToggle.textContent = '☀️'; | |
| localStorage.setItem('nightMode', 'true'); | |
| } else { | |
| themeToggle.textContent = '🌙'; | |
| localStorage.setItem('nightMode', 'false'); | |
| } | |
| }); | |
| // إظهار/إخفاء الإعدادات | |
| themeToggle.addEventListener('contextmenu', function (e) { | |
| e.preventDefault(); | |
| settingsPanel.classList.toggle('show'); | |
| }); | |
| // التحقق من دعم التعرف على الصوت | |
| if (!('webkitSpeechRecognition' in window || 'SpeechRecognition' in window)) { | |
| status.textContent = "متصفحك لا يدعم التعرف على الصوت"; | |
| browserWarning.style.display = 'block'; | |
| } else { | |
| const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; | |
| const recognition = new SpeechRecognition(); | |
| recognition.lang = 'ar-SA'; | |
| recognition.continuous = true; // <--- خليناها true عشان يستمر بالاستماع | |
| recognition.interimResults = false; | |
| let isListening = false; | |
| let isSpeaking = false; // حالة جديدة لتتبع إذا كان المساعد يرد | |
| let recognitionTimeout; // لتخزين معرف الـ timeout | |
| function startRecognition() { | |
| if (!isListening && !isSpeaking) { // لا تبدأ إذا كان يستمع أو يرد بالفعل | |
| try { | |
| recognition.start(); | |
| status.textContent = "أستمع إليك..."; | |
| status.classList.add('listening-text'); | |
| micCircle.classList.add('listening'); | |
| wave.classList.remove('hidden'); | |
| isListening = true; | |
| console.log("Recognition started."); | |
| } catch (e) { | |
| console.warn("Recognition start failed, might be already active or an error occurred:", e); | |
| // ممكن يكون شغال بالفعل، ما نسوي شي | |
| } | |
| } | |
| } | |
| // أول ما تفتح الصفحة، يبدأ الاستماع | |
| startRecognition(); | |
| micCircle.addEventListener('click', async function () { | |
| // إذا كان يضغط وهو يتكلم، نوقف المساعد الصوتي | |
| if (isSpeaking) { | |
| // هنا ممكن نضيف لوجيك لإيقاف الصوت إذا كان يتكلم | |
| console.log("User clicked while speaking, stopping current speech."); | |
| // على سبيل المثال، إيقاف أي صوت شغال | |
| if (window.currentAudioElement) { | |
| window.currentAudioElement.pause(); | |
| window.currentAudioElement.currentTime = 0; | |
| } | |
| // ونرجع المايك لحالة الاستعداد | |
| isSpeaking = false; | |
| clearInterval(window.speakingAnim); | |
| micCircle.classList.remove('speaking'); | |
| micCircle.style.transform = "scale(1)"; | |
| wave.classList.add('hidden'); | |
| status.textContent = "تم إيقاف الرد. انقر للتحدث مرة أخرى."; | |
| // ثم نحاول نبدأ الاستماع مرة أخرى | |
| startRecognition(); | |
| } else if (isListening) { | |
| // إذا كان يستمع وضغط، يوقف الاستماع | |
| recognition.stop(); | |
| console.log("Recognition stopped by user click."); | |
| } else { | |
| // إذا ما كان يستمع ولا يتكلم، نبدأ الاستماع | |
| startRecognition(); | |
| } | |
| }); | |
| recognition.onresult = async function (event) { | |
| // هنا إذا كان الـ recognition continuous، ممكن يعطي نتائج فارغة أو نتائج جزئية | |
| // نتأكد إن فيه كلام مفيد قبل ما نعالجه | |
| if (event.results.length === 0 || event.results[0].isFinal === false) { | |
| return; // تجاهل النتائج الجزئية أو الفارغة | |
| } | |
| const transcript = event.results[0][0].transcript; | |
| console.log(`Transcript: "${transcript}"`); | |
| // إذا كان الكلام قصير جداً أو مجرد ضوضاء، ممكن نتجاهله | |
| if (transcript.trim().length < 2) { // على سبيل المثال، أقل من حرفين | |
| status.textContent = "لم أسمع شيئاً واضحاً. حاول مرة أخرى."; | |
| micCircle.classList.remove('listening'); | |
| status.classList.remove('listening-text'); | |
| wave.classList.add('hidden'); | |
| isListening = false; // نعتبره خلص عشان يعيد الاستماع | |
| startRecognition(); // نعيد تشغيل الاستماع تلقائيًا | |
| return; | |
| } | |
| status.textContent = `تم الاستماع إلى: "${transcript}"`; | |
| status.classList.remove('listening-text'); | |
| // إيقاف الاستماع مؤقتاً أثناء الرد | |
| recognition.stop(); | |
| isListening = false; | |
| micCircle.classList.remove('listening'); | |
| wave.classList.add('hidden'); | |
| console.log("Recognition temporarily stopped for processing/speaking."); | |
| // إضافة حركة الرد | |
| micCircle.classList.add('speaking'); | |
| isSpeaking = true; | |
| // تحريك الدائرة ديناميكيًا أثناء الكلام | |
| let scale = 1; | |
| let direction = 1; | |
| window.speakingAnim = setInterval(() => { // استخدمنا window عشان نقدر نوقفها من أي مكان | |
| scale += 0.01 * direction; | |
| if (scale > 1.1) direction = -1; | |
| if (scale < 0.9) direction = 1; | |
| micCircle.style.transform = `scale(${scale})`; | |
| }, 16); | |
| try { | |
| const reply = await getGPTResponse(transcript); | |
| await speakResponse(reply); | |
| } catch (error) { | |
| console.error('Error in processing or speaking:', error); | |
| status.textContent = "حدث خطأ في المعالجة"; | |
| clearInterval(window.speakingAnim); | |
| micCircle.classList.remove('speaking'); | |
| micCircle.style.transform = "scale(1)"; | |
| isSpeaking = false; | |
| startRecognition(); // إعادة تشغيل الاستماع | |
| } | |
| } | |
| recognition.onend = function () { | |
| console.log("Recognition onend triggered."); | |
| if (!isSpeaking) { // إذا ما كان المساعد يرد حالياً | |
| // إذا انتهى الاستماع، ممكن يكون عشان خلص الكلام أو صار timeout | |
| // نعيد تشغيل الاستماع إذا ما كنا في حالة رد | |
| micCircle.classList.remove('listening'); | |
| status.classList.remove('listening-text'); | |
| wave.classList.add('hidden'); | |
| isListening = false; | |
| status.textContent = "انقر على الدائرة للتحدث"; // نص افتراضي | |
| startRecognition(); // <--- إعادة تشغيل الاستماع بعد الانتهاء | |
| } | |
| } | |
| recognition.onerror = function (event) { | |
| console.error('خطأ في التعرف على الصوت:', event.error); | |
| status.textContent = "حدث خطأ: " + event.error + ". حاول مرة أخرى."; | |
| status.classList.remove('listening-text'); | |
| micCircle.classList.remove('listening'); | |
| wave.classList.add('hidden'); | |
| isListening = false; | |
| isSpeaking = false; | |
| clearInterval(window.speakingAnim); // تأكد من إيقاف أي أنيميشن | |
| micCircle.classList.remove('speaking'); | |
| micCircle.style.transform = "scale(1)"; | |
| startRecognition(); // <--- إعادة تشغيل الاستماع حتى لو كان فيه خطأ | |
| } | |
| async function speakResponse(text) { | |
| try { | |
| const voice = document.getElementById("voiceSelect").value; | |
| const speed = parseFloat(document.getElementById("speedRange").value); | |
| const response = await fetch("https://lahja-dev-resource.cognitiveservices.azure.com/openai/deployments/LAHJA-V1/audio/speech?api-version=2025-03-01-preview", { | |
| method: "POST", | |
| headers: { | |
| "Content-Type": "application/json", | |
| "Authorization": "Bearer 4AwsIf87cyBIgaJVsy0phWUQdZFcbrJxpQBDQNzL4xjcP2MFzrrYJQQJ99BIACHYHv6XJ3w3AAAAACOGYrzM" | |
| }, | |
| body: JSON.stringify({ | |
| model: "LAHJA-V1", | |
| input: text, | |
| voice: voice, | |
| speed: 0.8 | |
| }) | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`TTS error! status: ${response.status}`); | |
| } | |
| const audioBlob = await response.blob(); | |
| const audioUrl = URL.createObjectURL(audioBlob); | |
| const audioElem = new Audio(audioUrl); | |
| window.currentAudioElement = audioElem; // لتتبع العنصر الصوتي الحالي | |
| status.textContent = "جاري الرد..."; | |
| audioElem.play().then(() => { | |
| audioElem.onended = () => { | |
| console.log("Speech finished."); | |
| clearInterval(window.speakingAnim); | |
| micCircle.classList.remove('speaking'); | |
| micCircle.style.transform = "scale(1)"; | |
| wave.classList.add('hidden'); | |
| isSpeaking = false; // انتهى الرد | |
| status.textContent = "انقر على الدائرة للتحدث مرة أخرى"; // نص مؤقت قبل إعادة الاستماع | |
| startRecognition(); // <--- إعادة تشغيل الاستماع تلقائيًا بعد انتهاء الرد | |
| }; | |
| }).catch(e => { | |
| console.error('Play error:', e); | |
| status.textContent = "خطأ في تشغيل الصوت. اضغط تشغيل لسماع الرد"; | |
| clearInterval(window.speakingAnim); | |
| micCircle.classList.remove('speaking'); | |
| micCircle.style.transform = "scale(1)"; | |
| wave.classList.add('hidden'); | |
| isSpeaking = false; | |
| startRecognition(); // إعادة تشغيل الاستماع | |
| }); | |
| } catch (error) { | |
| console.error('Error in speakResponse:', error); | |
| status.textContent = "خطأ في تحويل النص إلى صوت"; | |
| clearInterval(window.speakingAnim); | |
| micCircle.classList.remove('speaking'); | |
| micCircle.style.transform = "scale(1)"; | |
| wave.classList.add('hidden'); | |
| isSpeaking = false; | |
| startRecognition(); // إعادة تشغيل الاستماع | |
| } | |
| } | |
| async function getGPTResponse(text) { | |
| try { | |
| status.textContent = 'جاري التواصل مع المساعد...'; | |
| const response = await fetch("https://lahja-dev-resource.cognitiveservices.azure.com/openai/deployments/gpt-4o/chat/completions?api-version=2025-01-01-preview", { | |
| method: "POST", | |
| headers: { | |
| "Content-Type": "application/json", | |
| "Authorization": "Bearer 4AwsIf87cyBIgaJVsy0phWUQdZFcbrJxpQBDQNzL4xjcP2MFzrrYJQQJ99BIACHYHv6XJ3w3AAAAACOGYrzM" | |
| }, | |
| body: JSON.stringify({ | |
| messages: [ | |
| { | |
| "role": "system", | |
| "content": "انت نموذج اسمه (لهجة) مطور من قبل شركة أسس الذكاء الرقمي. رد باللهجة النجدية بطريقة ودية وطبيعية في جميع المحادثات." | |
| }, | |
| { | |
| "role": "user", | |
| "content":"دائما الاجابات بصوره مختصر لا تتجاوز سطر ودائما باللهجة النجدية " | |
| } | |
| , | |
| { | |
| "role": "user", | |
| "content":+ text | |
| } | |
| ], | |
| max_tokens: 4096, | |
| temperature: 0.8, | |
| top_p: 1, | |
| model: "gpt-4o" | |
| }) | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`HTTP error! status: ${response.status}`); | |
| } | |
| const data = await response.json(); | |
| return data.choices[0].message.content; | |
| } catch (error) { | |
| console.error('Error getting GPT response:', error); | |
| // رد افتراضي في حالة الخطأ | |
| return "الله يوفقك يا طيب! والله ما قدرت أفهم كلامك تمام، حاول مرة ثانية وخذ راحتك بالكلام."; | |
| } | |
| } | |
| } | |
| // إغلاق الإعدادات عند النقر خارجها | |
| document.addEventListener('click', function (e) { | |
| if (!settingsPanel.contains(e.target) && e.target !== themeToggle) { | |
| settingsPanel.classList.remove('show'); | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> |