/
godspace
/
voice-analytics
Обзор
Документация
Войти
/
godspace
/
voice-analytics
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
student.html
362 строки
17 KB
godspace
teachres ID
13 май 2026, 22:00
Верифицирован
13 май 2026, 22:00
8fa1d11
Код
Авторство
О чём код?
<!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Analytics System | Вход на занятие</title> <link rel="icon" href="data:,"> <script type="text/javascript" > (function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)}; m[i].l=1*new Date(); for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }} k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)}) (window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym"); ym(109134593, "init", { clickmap:true, trackLinks:true, accurateTrackBounce:true, webvisor:true }); </script> <noscript><div><img src="https://mc.yandex.ru/watch/109134593" style="position:absolute; left:-9999px;" alt="" /></div></noscript> <style> :root { --bg: #f8fafc; --card: #ffffff; --primary: #7b61ff; --text-main: #1e293b; --text-muted: #64748b; --border: #e2e8f0; --danger: #ef4444; --danger-bg: #fee2e2; } body { font-family: 'Segoe UI', system-ui, sans-serif; background-color: var(--bg); color: var(--text-main); margin: 0; padding: 1rem; display: flex; justify-content: center; align-items: center; min-height: 100vh; } .container { background: var(--card); width: 100%; max-width: 400px; border-radius: 20px; box-shadow: 0 10px 25px rgba(0,0,0,0.05); padding: 2rem; box-sizing: border-box; text-align: center; } h2 { margin: 0 0 1.5rem 0; font-size: 1.4rem; } .form-group { text-align: left; margin-bottom: 1.5rem; } .btn { background-color: var(--primary); color: white; border: none; padding: 16px; border-radius: 12px; font-size: 1.1rem; font-weight: bold; width: 100%; cursor: pointer; transition: 0.2s; } .btn:disabled { background-color: var(--border); cursor: not-allowed; } .btn-stop { background-color: var(--danger-bg); color: #991b1b; padding: 12px; font-size: 1rem; margin-top: 2rem; } .btn-stop:hover { background-color: #fca5a5; } /* Состояния панелей */ .active-ui, .finished-ui, .unsupported-ui { display: none; } .pulse-wrapper { width: 70px; height: 70px; background: rgba(123, 97, 255, 0.1); border-radius: 50%; display: flex; justify-content: center; align-items: center; margin: 0 auto 1.5rem auto; position: relative; } .pulse-dot { width: 30px; height: 30px; background-color: var(--primary); border-radius: 50%; animation: pulse 2s infinite; } @keyframes pulse { 0% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(123, 97, 255, 0.7); } 70% { transform: scale(1); box-shadow: 0 0 0 15px rgba(123, 97, 255, 0); } 100% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(123, 97, 255, 0); } } #loading-text { color: var(--text-muted); font-size: 0.9rem; } /* Стили для алерта (Пинга от учителя) */ #ping-alert { display: none; position: fixed; top: 20px; left: 50%; transform: translateX(-50%); background: #ef4444; color: white; padding: 15px 25px; border-radius: 10px; box-shadow: 0 10px 25px rgba(239,68,68,0.4); z-index: 9999; font-weight: bold; align-items: center; gap: 15px; width: 90%; max-width: 400px; } /* Плашка с именем ученика */ .student-badge { display: inline-block; background: #e0e7ff; color: var(--primary); padding: 8px 20px; border-radius: 20px; font-weight: bold; margin-bottom: 20px; font-size: 1.1rem; border: 1px solid #c7d2fe; } </style> </head> <body> <div id="ping-alert"> <div style="font-size: 24px;">🔔</div> <div style="text-align: left; flex-grow: 1;">Преподаватель просит проверить микрофон! Запись не идет.</div> <button onclick="document.getElementById('ping-alert').style.display='none'" style="background: white; color: #ef4444; border: none; border-radius: 5px; padding: 8px 12px; cursor: pointer; font-weight: bold;">Понял</button> </div> <div class="container" id="app-card"> <div id="unsupported-ui" class="unsupported-ui"> <div style="font-size: 60px; margin-bottom: 1rem;">🛑</div> <h2 style="color: var(--danger);">Браузер не подходит</h2> <p style="color: var(--text-muted); line-height: 1.5;"> Эта система работает только в <b>Google Chrome</b> или <b>Яндекс Браузере</b>.<br><br> Пожалуйста, скопируй ссылку и открой её там. </p> </div> <div id="setup-ui"> <h2 id="group-name">Загрузка...</h2> <div id="loading-text">Связь с облаком...</div> <div id="selection-area" style="display:none;"> <p style="color: var(--text-muted); font-size: 0.9rem; margin-bottom: 10px;">Вы вошли как:</p> <div class="student-badge" id="student-name-display">Имя Ученика</div> <button class="btn" id="start-btn">Начать занятие</button> </div> </div> <div id="active-ui" class="active-ui"> <div class="pulse-wrapper"> <div class="pulse-dot"></div> </div> <h2 style="margin-bottom: 0.5rem;">Вы в эфире</h2> <p style="color: var(--text-muted); font-size: 0.9rem; margin-bottom: 0;"> Система помогает учителю фиксировать ваши успехи. <br> Просто выполняйте задания в Алгоритмике. </p> <button class="btn btn-stop" id="manual-stop-btn">⏹ Завершить занятие</button> </div> <div id="finished-ui" class="finished-ui"> <div style="font-size: 60px; margin-bottom: 1rem;">🏁</div> <h2 id="finished-title" style="margin-bottom: 0.5rem; color: #10b981;">Урок завершен</h2> <p id="finished-text" style="color: var(--text-muted); font-size: 0.9rem; line-height: 1.5; margin-bottom: 0;"> Запись успешно остановлена.<br><br> Отличная работа! Можешь смело закрывать эту вкладку. </p> </div> </div> <script> const YANDEX_CLOUD_WEBHOOK = 'https://functions.yandexcloud.net/d4eode3bboka90viqg6u'; const MAX_LESSON_TIME = 100 * 60 * 1000; // 100 минут const setupUi = document.getElementById('setup-ui'); const activeUi = document.getElementById('active-ui'); const finishedUi = document.getElementById('finished-ui'); const unsupportedUi = document.getElementById('unsupported-ui'); // Фейсконтроль браузера const isSpeechSupported = 'webkitSpeechRecognition' in window || 'SpeechRecognition' in window; const isSafari = navigator.userAgent.indexOf("Safari") !== -1 && navigator.userAgent.indexOf("Chrome") === -1; if (!isSpeechSupported || isSafari) { setupUi.style.display = 'none'; unsupportedUi.style.display = 'block'; throw new Error("Браузер не поддерживается."); } const urlParams = new URLSearchParams(window.location.search); const teacherParam = urlParams.get('t') || 'default_teacher'; // Получаем ID учителя из ссылки const groupId = urlParams.get('group'); const studentParam = urlParams.get('s'); // Теперь мы получаем ID или Имя из ссылки const groupNameLabel = document.getElementById('group-name'); const loadingText = document.getElementById('loading-text'); const selectionArea = document.getElementById('selection-area'); const startBtn = document.getElementById('start-btn'); const manualStopBtn = document.getElementById('manual-stop-btn'); let recognition; let isRecording = false; let textBuffer = []; let autoStopTimer = null; let sendLogsInterval = null; let validStudentId = null; async function loadConfig() { if (!groupId || !studentParam) { groupNameLabel.innerText = "Ошибка доступа"; loadingText.innerText = "Недействительная персональная ссылка. Обратитесь к преподавателю."; loadingText.style.color = "var(--danger)"; return; } try { // Добавлен teacher_id в запрос const response = await fetch(`${YANDEX_CLOUD_WEBHOOK}?action=get_config&teacher_id=${teacherParam}`); const data = await response.json(); const config = data.config || {}; if (config[groupId]) { const group = config[groupId]; let cleanName = null; // Умная проверка: поддержка нового формата {ID: Имя} и старого (Массив) if (!Array.isArray(group.students) && typeof group.students === 'object') { // Если база обновлена до словарей (ID) if (group.students[studentParam]) { validStudentId = studentParam; cleanName = group.students[studentParam]; } } else { // Обратная совместимость для старых ссылок с именами const decodedName = decodeURIComponent(studentParam).replace(/_/g, ' '); if (group.students.includes(decodedName) || group.students.includes(studentParam)) { validStudentId = studentParam.replace(/\s+/g, '_'); cleanName = decodedName; } } if (cleanName) { groupNameLabel.innerText = group.name; loadingText.style.display = 'none'; document.getElementById('student-name-display').innerText = cleanName; selectionArea.style.display = 'block'; } else { groupNameLabel.innerText = "Ученик не найден"; loadingText.innerText = "Вашего профиля нет в списке этой группы."; loadingText.style.color = "var(--danger)"; } } else { groupNameLabel.innerText = "Группа не найдена"; loadingText.innerText = "Проверьте правильность ссылки."; loadingText.style.color = "var(--danger)"; } } catch (e) { groupNameLabel.innerText = "Ошибка подключения"; loadingText.innerText = "Не удалось загрузить данные из облака."; } } const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; recognition = new SpeechRecognition(); recognition.continuous = true; recognition.interimResults = false; recognition.lang = 'ru-RU'; // Отлов аппаратных ошибок микрофона recognition.onerror = (event) => { if (event.error === 'not-allowed' || event.error === 'audio-capture') { document.getElementById('finished-title').innerText = "Ошибка микрофона"; document.getElementById('finished-title').style.color = "var(--danger)"; document.getElementById('finished-text').innerHTML = "Браузер потерял доступ к вашему микрофону.<br><br>Проверьте штекер гарнитуры и настройки Windows, затем обновите страницу."; forceStopRecording(false); } }; recognition.onresult = (event) => { let chunk = ''; for (let i = event.resultIndex; i < event.results.length; ++i) { if (event.results[i].isFinal) { chunk += event.results[i][0].transcript.trim() + '. '; } } if (chunk) { textBuffer.push({ timestamp: new Date().toISOString(), text: chunk }); } }; recognition.onend = () => { if (isRecording) { setTimeout(() => { try { recognition.start(); } catch(e) {} }, 500); } }; startBtn.onclick = () => { if (!validStudentId) return; try { recognition.start(); isRecording = true; setupUi.style.display = 'none'; activeUi.style.display = 'block'; sendLogsInterval = setInterval(sendLogs, 15000); autoStopTimer = setTimeout(() => forceStopRecording(true), MAX_LESSON_TIME); if (typeof ym !== 'undefined') { ym(109134593, 'reachGoal', 'lesson_started'); } } catch(e) { alert("Ошибка: Разрешите доступ к микрофону"); } }; manualStopBtn.onclick = () => forceStopRecording(false); function forceStopRecording(isAuto) { if (!isRecording) return; isRecording = false; if (recognition) recognition.stop(); if (autoStopTimer) clearTimeout(autoStopTimer); if (sendLogsInterval) clearInterval(sendLogsInterval); sendLogs(); activeUi.style.display = 'none'; finishedUi.style.display = 'block'; if (typeof ym !== 'undefined') { ym(109134593, 'reachGoal', isAuto ? 'lesson_auto_finished' : 'lesson_manual_finished'); } } async function sendLogs() { // Шлем данные каждые 15 сек для поддержки "зеленой лампочки" у учителя const payload = { action: 'save_logs', teacher_id: teacherParam, // Отправляем ID учителя в облако group_id: groupId, student_id: validStudentId, // Отправляем ID session_date: new Date().toISOString().split('T')[0], transcripts: [...textBuffer] }; textBuffer = []; try { const response = await fetch(YANDEX_CLOUD_WEBHOOK, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const data = await response.json(); // ПРОВЕРКА ПИНГА ОТ УЧИТЕЛЯ if (data.ping) { document.getElementById('ping-alert').style.display = 'flex'; } } catch (e) { if (payload.transcripts.length > 0) { textBuffer = [...payload.transcripts, ...textBuffer]; } } } loadConfig(); </script> </body> </html>