/
godspace
/
voice-analytics
Обзор
Документация
Войти
/
godspace
/
voice-analytics
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
admin.html
691 строка
29 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>Панель руководителя - Аналитика уроков</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?id=109134593', 'ym'); ym(109134593, 'init', { ssr:true, webvisor:true, clickmap:true, ecommerce:"dataLayer", referrer: document.referrer, url: location.href, accurateTrackBounce:true, trackLinks:true }); </script> <noscript><div><img src="https://mc.yandex.ru/watch/109134593" style="position:absolute; left:-9999px;" alt="" /></div></noscript> <style> body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin: 0; padding: 20px; background-color: #f4f7f6; color: #333; } .container { max-width: 800px; margin: auto; background: white; padding: 30px; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.05); position: relative; } h1 { color: #2c3e50; margin-top: 0; } p.subtitle { color: #7f8c8d; margin-bottom: 25px; } .form-group { margin-bottom: 20px; } label { font-weight: bold; display: block; margin-bottom: 8px; color: #34495e; } select, input[type="date"] { width: 100%; padding: 12px; font-size: 16px; border-radius: 8px; border: 1px solid #cbd5e1; outline: none; background-color: #f8fafc; box-sizing: border-box; } select:focus, input[type="date"]:focus { border-color: #3498db; } .student-card { border: 1px solid #e2e8f0; padding: 20px; margin-bottom: 15px; border-radius: 10px; background: #ffffff; display: flex; flex-direction: column; } .student-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; flex-wrap: wrap; gap: 10px; } .student-header h3 { margin: 0; color: #2c3e50; } .btn-group { display: flex; gap: 10px; } button.generate-btn { background-color: #8e44ad; color: white; border: none; padding: 10px 20px; font-size: 14px; border-radius: 6px; cursor: pointer; transition: background 0.3s; font-weight: bold; width: 100%; } button.generate-btn:hover { background-color: #732d91; } button.generate-btn.small { width: auto; } button.generate-btn:disabled { background-color: #bdc3c7; cursor: not-allowed; } .report-box { margin-top: 15px; padding: 20px; background: #f8f9fa; border-left: 5px solid #2ecc71; border-radius: 4px; display: none; white-space: pre-wrap; line-height: 1.6; color: #2c3e50; font-size: 15px; } .error-box { border-left-color: #e74c3c; background: #fdf2f0; } .loader { display: none; color: #8e44ad; font-weight: bold; font-size: 14px; margin-top: 10px; animation: pulse 1.5s infinite; } @keyframes pulse { 0% { opacity: 0.6; } 50% { opacity: 1; } 100% { opacity: 0.6; } } .tabs { display: flex; gap: 10px; margin-bottom: 25px; border-bottom: 2px solid #e2e8f0; padding-bottom: 10px; } .tab-btn { padding: 10px 20px; border: none; background: none; cursor: pointer; font-size: 16px; font-weight: bold; color: #94a3b8; transition: 0.3s; } .tab-btn.active { color: #8e44ad; border-bottom: 3px solid #8e44ad; margin-bottom: -13px; } .tab-content { display: none; animation: fadeIn 0.3s; } .tab-content.active { display: block; } @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } .grid-dates { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; } /* --- Стили для окна авторизации --- */ #auth-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(15, 23, 42, 0.8); backdrop-filter: blur(4px); display: flex; justify-content: center; align-items: center; z-index: 9999; } .auth-card { background: white; padding: 2.5rem 2rem; border-radius: 16px; width: 90%; max-width: 420px; text-align: center; box-shadow: 0 20px 25px -5px rgba(0,0,0,0.1); } .auth-card h2 { margin-top: 0; color: #2c3e50; font-size: 1.5rem; margin-bottom: 10px; } .auth-card p { color: #7f8c8d; margin-bottom: 25px; font-size: 0.95rem; line-height: 1.5; } .user-badge { font-size: 0.85rem; background: #f3e8ff; color: #4f46e5; padding: 6px 14px; border-radius: 20px; font-weight: bold; display: flex; align-items: center; gap: 10px; } .logout-btn { background: none; border: none; color: #64748b; cursor: pointer; padding: 0; font-size: 0.85rem; text-decoration: underline; } .logout-btn:hover { color: #334155; } .header-top { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 5px; } </style> </head> <body> <div id="auth-overlay"> <div class="auth-card"> <div style="font-size: 45px; margin-bottom: 15px;">📊</div> <h2>Вход в аналитику</h2> <p>Введите ваш ID преподавателя, чтобы получить доступ к отчетам ваших групп.</p> <input type="text" id="teacher-id-input" placeholder="Например: ivanov" style="text-align:center; font-weight:bold; letter-spacing:1px; margin-bottom: 20px; width: 100%; padding: 14px; border: 2px solid #cbd5e1; border-radius: 8px; outline: none; box-sizing: border-box;"> <button class="generate-btn" style="width:100%; padding: 14px; font-size: 16px;" onclick="loginTeacher()">Войти в панель</button> </div> </div> <div class="container" id="app-container" style="display:none;"> <div class="header-top"> <div> <h1>📊 Аналитика уроков</h1> <p class="subtitle" style="margin-top: 5px;">Генерация обратной связи для родителей на базе искусственного интеллекта YandexGPT</p> </div> <div class="user-badge"> <span id="current-user-display">Преподаватель</span> <button class="logout-btn" onclick="logoutTeacher()">Сменить</button> </div> </div> <div class="tabs"> <button class="tab-btn active" onclick="openTab('daily')">Отчеты за день</button> <button class="tab-btn" onclick="openTab('period')">Аналитика за период</button> </div> <div id="daily" class="tab-content active"> <div class="form-group"> <label for="groupSelect">Выберите группу:</label> <select id="groupSelect" onchange="loadDates()"> <option value="">-- Загрузка групп... --</option> </select> </div> <div class="form-group"> <label for="dateSelect">Выберите дату занятия:</label> <select id="dateSelect" onchange="renderStudents()" disabled> <option value="">-- Сначала выберите группу --</option> </select> </div> <div id="studentsContainer" style="margin-top: 30px;"></div> </div> <div id="period" class="tab-content"> <div class="form-group"> <label for="groupSelectPeriod">Выберите группу:</label> <select id="groupSelectPeriod" onchange="loadStudentsPeriod()"> <option value="">-- Загрузка групп... --</option> </select> </div> <div class="form-group"> <label for="studentSelectPeriod">Выберите ученика:</label> <select id="studentSelectPeriod" disabled> <option value="">-- Сначала выберите группу --</option> </select> </div> <div class="grid-dates form-group"> <div> <label>Период С:</label> <input type="date" id="dateFrom"> </div> <div> <label>Период ПО:</label> <input type="date" id="dateTo"> </div> </div> <button class="generate-btn" id="generatePeriodBtn" onclick="generatePeriodReport()">📈 Сгенерировать итоговый отчет</button> <div id="periodLoader" class="loader">🤖 Старший методист собирает данные и пишет итоговое ревью...</div> <div id="periodReportBox" class="report-box"></div> </div> </div> <script> const WEBHOOK_URL = 'https://functions.yandexcloud.net/d4eode3bboka90viqg6u'; let globalConfig = {}; let allGroupDates = []; // --- ЛОГИКА АВТОРИЗАЦИИ --- let CURRENT_TEACHER_ID = localStorage.getItem('voice_analytics_teacher_id'); function checkAuth() { if (CURRENT_TEACHER_ID) { document.getElementById('auth-overlay').style.display = 'none'; document.getElementById('app-container').style.display = 'block'; document.getElementById('current-user-display').innerText = CURRENT_TEACHER_ID; init(); // Загружаем данные только после авторизации } else { document.getElementById('auth-overlay').style.display = 'flex'; document.getElementById('app-container').style.display = 'none'; } } function loginTeacher() { const input = document.getElementById('teacher-id-input').value.trim(); if (!input) return alert("Пожалуйста, введите ваш ID"); const cleanId = input.replace(/[^a-zA-Z0-9_-]/g, '').toLowerCase(); localStorage.setItem('voice_analytics_teacher_id', cleanId); CURRENT_TEACHER_ID = cleanId; checkAuth(); } function logoutTeacher() { localStorage.removeItem('voice_analytics_teacher_id'); window.location.reload(); } // Проверяем авторизацию при загрузке страницы checkAuth(); function openTab(tabId) { document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active')); document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); document.getElementById(tabId).classList.add('active'); event.currentTarget.classList.add('active'); } async function init() { const groupSelect = document.getElementById('groupSelect'); const groupSelectPeriod = document.getElementById('groupSelectPeriod'); try { const res = await fetch(`${WEBHOOK_URL}?action=get_config&teacher_id=${CURRENT_TEACHER_ID}`); const data = await res.json(); globalConfig = data.config || {}; const optionsHTML = '<option value="">-- Выберите группу --</option>' + Object.keys(globalConfig).map(id => `<option value="${id}">${globalConfig[id].name} (ID: ${id})</option>`).join(''); groupSelect.innerHTML = optionsHTML; groupSelectPeriod.innerHTML = optionsHTML; } catch (e) { groupSelect.innerHTML = '<option value="">Ошибка загрузки базы данных</option>'; groupSelectPeriod.innerHTML = '<option value="">Ошибка загрузки базы данных</option>'; } } async function loadDates() { const groupId = document.getElementById('groupSelect').value; const dateSelect = document.getElementById('dateSelect'); document.getElementById('studentsContainer').innerHTML = ''; if (!groupId) { dateSelect.innerHTML = '<option value="">-- Сначала выберите группу --</option>'; dateSelect.disabled = true; return; } dateSelect.innerHTML = '<option value="">Загрузка сохраненных уроков...</option>'; dateSelect.disabled = true; try { const res = await fetch(`${WEBHOOK_URL}?action=get_dates&teacher_id=${CURRENT_TEACHER_ID}&group_id=${groupId}`); const data = await res.json(); if (data.dates && data.dates.length > 0) { dateSelect.innerHTML = '<option value="">-- Выберите дату урока --</option>'; data.dates.sort().reverse().forEach(date => { const opt = document.createElement('option'); opt.value = date; const [y, m, d] = date.split('-'); opt.textContent = `${d}.${m}.${y}`; dateSelect.appendChild(opt); }); dateSelect.disabled = false; } else { dateSelect.innerHTML = '<option value="">Нет записанных уроков для этой группы</option>'; } } catch (e) { dateSelect.innerHTML = '<option value="">Ошибка загрузки дат</option>'; } } function renderStudents() { const groupId = document.getElementById('groupSelect').value; const date = document.getElementById('dateSelect').value; const container = document.getElementById('studentsContainer'); container.innerHTML = ''; if (!groupId || !date) return; // --- КАРТОЧКА ПРЕПОДАВАТЕЛЯ --- const teacherCard = document.createElement('div'); teacherCard.className = 'student-card'; teacherCard.style.borderLeft = '5px solid #3498db'; teacherCard.innerHTML = ` <div class="student-header"> <h3 style="color: #2980b9;">👨🏫 Преподаватель (Анализ урока)</h3> <div class="btn-group"> <button class="generate-btn small" style="background-color: #2ecc71;" onclick="viewTeacherReport('${groupId}', '${date}', this)">👁️ Просмотреть</button> <button class="generate-btn small" style="background-color: #3498db;" onclick="generateTeacherReport('${groupId}', '${date}', this)">🔄 Обновить ОС (ИИ)</button> </div> </div> <span class="loader" style="color: #3498db;">🤖 Ожидайте...</span> <div class="report-box"></div> `; container.appendChild(teacherCard); // --- КАРТОЧКИ УЧЕНИКОВ --- const students = globalConfig[groupId].students || {}; const entries = Array.isArray(students) ? students.map(n => [n.replace(/\s+/g, '_'), n]) : Object.entries(students); entries.forEach(([id, name]) => { const cleanName = name.replace(/_/g, ' '); const card = document.createElement('div'); card.className = 'student-card'; card.innerHTML = ` <div class="student-header"> <h3>🎓 ${cleanName}</h3> <div class="btn-group"> <button class="generate-btn small" style="background-color: #2ecc71;" onclick="viewReport('${groupId}', '${date}', '${id}', this)">👁️ Просмотреть</button> <button class="generate-btn small" onclick="generateReport('${groupId}', '${date}', '${id}', this)">✨ Создать ОС (ИИ)</button> </div> </div> <span class="loader">🤖 Ожидайте...</span> <div class="report-box"></div> `; container.appendChild(card); }); } // --- ФУНКЦИИ ПРОСМОТРА СОХРАНЕННЫХ ОТЧЕТОВ --- async function viewReport(groupId, date, studentId, btnElement) { const card = btnElement.closest('.student-card'); const loader = card.querySelector('.loader'); const reportBox = card.querySelector('.report-box'); reportBox.style.display = 'none'; reportBox.classList.remove('error-box'); loader.textContent = '⏳ Ищем сохраненный отчет...'; loader.style.display = 'block'; try { const res = await fetch(WEBHOOK_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'get_saved_report', teacher_id: CURRENT_TEACHER_ID, group_id: groupId, date: date, student_id: studentId }) }); const data = await res.json(); if (data.report) { reportBox.textContent = data.report; reportBox.style.display = 'block'; } else { reportBox.textContent = "Отчет для этого ученика за выбранную дату еще не создавался. Нажмите «Создать ОС (ИИ)»."; reportBox.classList.add('error-box'); reportBox.style.display = 'block'; } } catch (e) { reportBox.textContent = "Произошла сетевая ошибка."; reportBox.classList.add('error-box'); reportBox.style.display = 'block'; } finally { loader.style.display = 'none'; } } async function viewTeacherReport(groupId, date, btnElement) { const card = btnElement.closest('.student-card'); const loader = card.querySelector('.loader'); const reportBox = card.querySelector('.report-box'); reportBox.style.display = 'none'; reportBox.classList.remove('error-box'); loader.textContent = '⏳ Ищем сохраненный отчет...'; loader.style.display = 'block'; try { const res = await fetch(WEBHOOK_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'get_saved_report', teacher_id: CURRENT_TEACHER_ID, group_id: groupId, date: date, student_id: 'Teacher_Methodology' }) }); const data = await res.json(); if (data.report) { reportBox.textContent = data.report; reportBox.style.display = 'block'; } else { reportBox.textContent = "Разбор урока еще не проводился. Нажмите «Обновить ОС (ИИ)»."; reportBox.classList.add('error-box'); reportBox.style.display = 'block'; } } catch (e) { reportBox.textContent = "Произошла сетевая ошибка."; reportBox.classList.add('error-box'); reportBox.style.display = 'block'; } finally { loader.style.display = 'none'; } } // --- ФУНКЦИИ ГЕНЕРАЦИИ (YandexGPT) --- async function generateReport(groupId, date, studentId, btnElement) { const card = btnElement.closest('.student-card'); const loader = card.querySelector('.loader'); const reportBox = card.querySelector('.report-box'); reportBox.style.display = 'none'; reportBox.classList.remove('error-box'); loader.textContent = '🤖 Нейросеть анализирует стенограмму...'; loader.style.display = 'block'; try { const res = await fetch(WEBHOOK_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'generate_report', teacher_id: CURRENT_TEACHER_ID, group_id: groupId, date: date, student_id: studentId }) }); const data = await res.json(); if (data.report) { reportBox.textContent = data.report; reportBox.style.display = 'block'; if (typeof ym !== 'undefined') ym(109134593, 'reachGoal', 'report_generated'); } else if (data.error) { reportBox.textContent = "Внимание: " + data.error; reportBox.classList.add('error-box'); reportBox.style.display = 'block'; } } catch (e) { reportBox.textContent = "Произошла сетевая ошибка."; reportBox.classList.add('error-box'); reportBox.style.display = 'block'; } finally { loader.style.display = 'none'; } } async function generateTeacherReport(groupId, date, btnElement) { const card = btnElement.closest('.student-card'); const loader = card.querySelector('.loader'); const reportBox = card.querySelector('.report-box'); reportBox.style.display = 'none'; reportBox.classList.remove('error-box'); loader.textContent = '🤖 Методист оценивает ход урока...'; loader.style.display = 'block'; try { const res = await fetch(WEBHOOK_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'generate_teacher_report', teacher_id: CURRENT_TEACHER_ID, group_id: groupId, date: date }) }); const data = await res.json(); if (data.report) { reportBox.textContent = data.report; reportBox.style.display = 'block'; if (typeof ym !== 'undefined') ym(109134593, 'reachGoal', 'teacher_report_generated'); } else if (data.error) { reportBox.textContent = "Внимание: " + data.error; reportBox.classList.add('error-box'); reportBox.style.display = 'block'; } } catch (e) { reportBox.textContent = "Произошла сетевая ошибка."; reportBox.classList.add('error-box'); reportBox.style.display = 'block'; } finally { loader.style.display = 'none'; } } async function loadStudentsPeriod() { const groupId = document.getElementById('groupSelectPeriod').value; const studentSelect = document.getElementById('studentSelectPeriod'); if (!groupId) { studentSelect.innerHTML = '<option value="">-- Сначала выберите группу --</option>'; studentSelect.disabled = true; return; } studentSelect.innerHTML = '<option value="">-- Выберите ученика --</option>'; const students = globalConfig[groupId].students || {}; const entries = Array.isArray(students) ? students.map(n => [n.replace(/\s+/g, '_'), n]) : Object.entries(students); entries.forEach(([id, name]) => { const opt = document.createElement('option'); opt.value = id; opt.textContent = name.replace(/_/g, ' '); studentSelect.appendChild(opt); }); studentSelect.disabled = false; try { const res = await fetch(`${WEBHOOK_URL}?action=get_dates&teacher_id=${CURRENT_TEACHER_ID}&group_id=${groupId}`); const data = await res.json(); allGroupDates = data.dates || []; } catch(e) {} } async function generatePeriodReport() { const groupId = document.getElementById('groupSelectPeriod').value; const studentId = document.getElementById('studentSelectPeriod').value; const dateFrom = document.getElementById('dateFrom').value; const dateTo = document.getElementById('dateTo').value; const btn = document.getElementById('generatePeriodBtn'); const loader = document.getElementById('periodLoader'); const reportBox = document.getElementById('periodReportBox'); if (!groupId || !studentId || !dateFrom || !dateTo) { alert("Пожалуйста, заполните все поля."); return; } if (dateFrom > dateTo) { alert("Начальная дата не может быть больше конечной!"); return; } const validDatesInPeriod = allGroupDates.filter(d => d >= dateFrom && d <= dateTo); if (validDatesInPeriod.length === 0) { alert("В выбранном диапазоне нет ни одного записанного урока."); return; } btn.disabled = true; reportBox.style.display = 'none'; reportBox.classList.remove('error-box'); loader.style.display = 'block'; try { const res = await fetch(WEBHOOK_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'generate_period_report', teacher_id: CURRENT_TEACHER_ID, group_id: groupId, student_id: studentId, dates: validDatesInPeriod }) }); const data = await res.json(); if (data.report) { reportBox.textContent = data.report; reportBox.style.display = 'block'; if (typeof ym !== 'undefined') ym(109134593, 'reachGoal', 'period_report_generated'); } else if (data.error) { reportBox.textContent = "Внимание: " + data.error; reportBox.classList.add('error-box'); reportBox.style.display = 'block'; } } catch (e) { reportBox.textContent = "Произошла сетевая ошибка."; reportBox.classList.add('error-box'); reportBox.style.display = 'block'; } finally { loader.style.display = 'none'; btn.disabled = false; } } </script> </body> </html>