29aa985504
- New js/sound.js: shared LS.sfx module with 21 synthesized sounds (ADSR envelope, sequences, sweeps, noise) - Classroom: lesson_start/end, user_joined/left, hand_raise, chat_message, muted, draw_permitted - Dashboard: achievement, level_up, xp_gain, coin via SSE events - Live quiz: quiz_start, quiz_end on question launch and results - Settings panel: global enable toggle + volume slider + localStorage persistence - Replaces old _crBeep() in classroom.html Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
943 lines
43 KiB
HTML
943 lines
43 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>Live-квиз — LearnSpace</title>
|
||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||
<link href="https://fonts.googleapis.com/css2?family=Unbounded:wght@400;700;800&family=Manrope:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||
<link rel="stylesheet" href="/css/ls.css" />
|
||
<script src="https://cdn.jsdelivr.net/npm/lucide@0.469.0/dist/umd/lucide.min.js"></script>
|
||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css" />
|
||
<style>
|
||
.sb-content { background: #f4f5f8; overflow: hidden; display: flex; flex-direction: column; }
|
||
|
||
/* ── page header ── */
|
||
.lq-header {
|
||
background: linear-gradient(140deg, #0a0220 0%, #1a0a3c 60%, #080818 100%);
|
||
padding: 24px 28px 20px; position: relative; overflow: hidden; flex-shrink: 0;
|
||
}
|
||
.lq-header-dots {
|
||
position: absolute; inset: 0;
|
||
background-image: radial-gradient(circle, rgba(255,255,255,0.04) 1px, transparent 1px);
|
||
background-size: 22px 22px; pointer-events: none;
|
||
}
|
||
.lq-header-inner { position: relative; z-index: 1; }
|
||
.lq-title {
|
||
font-family: 'Unbounded', sans-serif; font-size: 1.2rem; font-weight: 800;
|
||
color: #fff; display: flex; align-items: center; gap: 10px;
|
||
}
|
||
.lq-session-chip {
|
||
display: inline-flex; align-items: center; gap: 6px;
|
||
padding: 5px 14px; border-radius: 999px;
|
||
background: rgba(6,214,160,0.15); border: 1.5px solid rgba(6,214,160,0.3);
|
||
font-size: 0.74rem; font-weight: 700; color: #06D6A0;
|
||
font-family: 'Unbounded', sans-serif; margin-top: 10px;
|
||
}
|
||
.lq-session-chip .dot {
|
||
width: 7px; height: 7px; border-radius: 50%; background: #06D6A0;
|
||
animation: pulse-dot 1.5s ease infinite;
|
||
}
|
||
@keyframes pulse-dot {
|
||
0%, 100% { opacity: 1; transform: scale(1); }
|
||
50% { opacity: 0.5; transform: scale(0.7); }
|
||
}
|
||
|
||
/* ── body ── */
|
||
.lq-body {
|
||
flex: 1; display: flex; gap: 0; overflow: hidden; min-height: 0;
|
||
}
|
||
|
||
/* ── left panel ── */
|
||
.lq-left {
|
||
width: 300px; flex-shrink: 0;
|
||
background: #fff; border-right: 1.5px solid rgba(15,23,42,0.08);
|
||
overflow-y: auto; display: flex; flex-direction: column;
|
||
}
|
||
.lq-panel-head {
|
||
padding: 16px 18px 12px;
|
||
border-bottom: 1px solid rgba(15,23,42,0.07);
|
||
font-family: 'Unbounded', sans-serif; font-size: 0.78rem; font-weight: 800;
|
||
color: #0F172A; display: flex; align-items: center; gap: 8px;
|
||
}
|
||
.class-list { padding: 10px 10px; display: flex; flex-direction: column; gap: 6px; flex: 1; }
|
||
.class-card {
|
||
border: 1.5px solid rgba(15,23,42,0.08); border-radius: 14px;
|
||
padding: 13px 14px; cursor: pointer; transition: all 0.15s;
|
||
display: flex; align-items: center; gap: 12px;
|
||
}
|
||
.class-card:hover { border-color: rgba(155,93,229,0.25); background: rgba(155,93,229,0.03); }
|
||
.class-card.active {
|
||
border-color: rgba(155,93,229,0.4); background: rgba(155,93,229,0.06);
|
||
box-shadow: 0 2px 10px rgba(155,93,229,0.1);
|
||
}
|
||
.class-card-icon {
|
||
width: 38px; height: 38px; border-radius: 10px;
|
||
background: linear-gradient(135deg, #9B5DE5, #06D6E0);
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-family: 'Unbounded', sans-serif; font-size: 0.72rem; font-weight: 800; color: #fff;
|
||
flex-shrink: 0;
|
||
}
|
||
.class-card-name { font-size: 0.85rem; font-weight: 700; color: #0F172A; }
|
||
.class-card-meta { font-size: 0.72rem; color: #8898AA; margin-top: 2px; }
|
||
|
||
.lq-session-area { padding: 14px 12px; border-top: 1px solid rgba(15,23,42,0.07); }
|
||
.btn-start {
|
||
width: 100%; padding: 12px; border: none; border-radius: 12px;
|
||
background: linear-gradient(135deg, #06D6E0, #9B5DE5);
|
||
font-family: 'Unbounded', sans-serif; font-size: 0.78rem; font-weight: 800;
|
||
color: #fff; cursor: pointer; transition: transform 0.15s, box-shadow 0.15s;
|
||
display: flex; align-items: center; justify-content: center; gap: 8px;
|
||
}
|
||
.btn-start:hover { transform: translateY(-1px); box-shadow: 0 6px 20px rgba(155,93,229,0.3); }
|
||
.btn-start:disabled { opacity: 0.5; cursor: default; transform: none; box-shadow: none; }
|
||
.btn-end {
|
||
width: 100%; padding: 10px; border: 1.5px solid rgba(239,71,111,0.3); border-radius: 12px;
|
||
background: rgba(239,71,111,0.06);
|
||
font-family: 'Unbounded', sans-serif; font-size: 0.76rem; font-weight: 800;
|
||
color: #EF476F; cursor: pointer; transition: all 0.15s; margin-top: 8px;
|
||
display: flex; align-items: center; justify-content: center; gap: 6px;
|
||
}
|
||
.btn-end:hover { background: rgba(239,71,111,0.12); border-color: #EF476F; }
|
||
.active-status {
|
||
padding: 10px 12px; border-radius: 12px;
|
||
background: rgba(6,214,160,0.08); border: 1.5px solid rgba(6,214,160,0.25);
|
||
margin-bottom: 8px;
|
||
}
|
||
.as-label { font-size: 0.7rem; font-weight: 700; color: #8898AA; margin-bottom: 3px; }
|
||
.as-val {
|
||
font-family: 'Unbounded', sans-serif; font-size: 0.82rem; font-weight: 800;
|
||
color: #059652; display: flex; align-items: center; gap: 6px;
|
||
}
|
||
.as-dot { width: 7px; height: 7px; border-radius: 50%; background: #06D6A0; animation: pulse-dot 1.5s ease infinite; }
|
||
|
||
/* ── right panel ── */
|
||
.lq-right { flex: 1; overflow-y: auto; padding: 20px 24px 60px; min-width: 0; }
|
||
|
||
/* question search */
|
||
.lq-search-wrap { position: relative; margin-bottom: 16px; }
|
||
.lq-search {
|
||
width: 100%; padding: 10px 14px 10px 40px;
|
||
border: 1.5px solid rgba(15,23,42,0.12); border-radius: 12px;
|
||
font-family: 'Manrope', sans-serif; font-size: 0.86rem; font-weight: 500;
|
||
color: #0F172A; background: #fff; outline: none; transition: border-color 0.15s;
|
||
}
|
||
.lq-search:focus { border-color: var(--violet); }
|
||
.lq-search-icon {
|
||
position: absolute; left: 13px; top: 50%; transform: translateY(-50%);
|
||
color: #8898AA; pointer-events: none; width: 16px; height: 16px;
|
||
}
|
||
|
||
/* section header */
|
||
.lq-section-title {
|
||
font-family: 'Unbounded', sans-serif; font-size: 0.78rem; font-weight: 800;
|
||
color: #0F172A; margin: 0 0 12px; display: flex; align-items: center; gap: 8px;
|
||
}
|
||
|
||
/* question pick cards */
|
||
.lq-q-list { display: flex; flex-direction: column; gap: 8px; margin-bottom: 24px; }
|
||
/* question filters */
|
||
.lq-filter-row { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
|
||
.lq-filter-select {
|
||
flex: 1; padding: 8px 10px;
|
||
border: 1.5px solid rgba(15,23,42,0.12); border-radius: 10px;
|
||
font-family: 'Manrope', sans-serif; font-size: 0.8rem; font-weight: 500;
|
||
color: #0F172A; background: #fff; outline: none; cursor: pointer;
|
||
transition: border-color 0.15s; appearance: none;
|
||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%238898AA' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
|
||
background-repeat: no-repeat; background-position: right 8px center; padding-right: 24px;
|
||
}
|
||
.lq-filter-select:focus { border-color: var(--violet); }
|
||
.lq-q-count { font-size: 0.7rem; color: #8898AA; font-weight: 600; white-space: nowrap; margin-bottom: 12px; }
|
||
|
||
/* load more */
|
||
.btn-load-more {
|
||
width: 100%; padding: 10px; border: 1.5px dashed rgba(155,93,229,0.3);
|
||
border-radius: 12px; background: transparent; margin-bottom: 24px;
|
||
font-family: 'Manrope', sans-serif; font-size: 0.82rem; font-weight: 700;
|
||
color: var(--violet); cursor: pointer; transition: all 0.15s;
|
||
}
|
||
.btn-load-more:hover { background: rgba(155,93,229,0.05); border-color: var(--violet); }
|
||
.btn-load-more:disabled { opacity: 0.5; cursor: default; }
|
||
|
||
/* result stat cards */
|
||
.lq-result-stats { display: flex; gap: 8px; margin-bottom: 14px; }
|
||
.lq-result-stat { flex: 1; padding: 10px 12px; border-radius: 12px; background: rgba(15,23,42,0.04); text-align: center; }
|
||
.lq-result-stat-val { font-family: 'Unbounded', sans-serif; font-size: 1.1rem; font-weight: 900; color: #0F172A; }
|
||
.lq-result-stat-lbl { font-size: 0.64rem; color: #8898AA; margin-top: 2px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; }
|
||
.lq-result-stat.rs-correct { background: rgba(6,214,160,0.08); }
|
||
.lq-result-stat.rs-correct .lq-result-stat-val { color: #059652; }
|
||
.lq-result-stat.rs-wrong { background: rgba(239,71,111,0.07); }
|
||
.lq-result-stat.rs-wrong .lq-result-stat-val { color: #EF476F; }
|
||
|
||
/* explanation box */
|
||
.lq-explanation {
|
||
margin-top: 14px; padding: 13px 15px;
|
||
background: rgba(6,214,224,0.06); border: 1.5px solid rgba(6,214,224,0.2);
|
||
border-radius: 12px; font-size: 0.82rem; color: #0F172A; line-height: 1.6;
|
||
}
|
||
.lq-explanation-label { font-size: 0.64rem; font-weight: 700; color: #0891B2; text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 6px; }
|
||
|
||
.lq-q-card {
|
||
background: #fff; border: 1.5px solid rgba(15,23,42,0.07);
|
||
border-radius: 14px; padding: 14px 16px;
|
||
display: flex; align-items: flex-start; gap: 12px;
|
||
box-shadow: 0 1px 4px rgba(15,23,42,0.04); transition: all 0.15s;
|
||
}
|
||
.lq-q-card:hover { border-color: rgba(155,93,229,0.25); }
|
||
.lq-q-card.launched { border-color: rgba(6,214,160,0.4); background: rgba(6,214,160,0.03); }
|
||
.lq-q-body { flex: 1; min-width: 0; }
|
||
.lq-q-text {
|
||
font-size: 0.84rem; font-weight: 600; color: #0F172A;
|
||
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
|
||
overflow: hidden; line-height: 1.5; min-height: 1.5em;
|
||
}
|
||
.lq-q-meta { font-size: 0.7rem; color: #8898AA; margin-top: 5px; display: flex; gap: 8px; flex-wrap: wrap; }
|
||
.btn-launch {
|
||
padding: 7px 16px; border: none; border-radius: 999px;
|
||
background: var(--grad-1); color: #fff;
|
||
font-family: 'Manrope', sans-serif; font-size: 0.78rem; font-weight: 800;
|
||
cursor: pointer; transition: transform 0.12s, box-shadow 0.12s; flex-shrink: 0;
|
||
display: flex; align-items: center; gap: 5px;
|
||
}
|
||
.btn-launch:hover { transform: translateY(-1px); box-shadow: 0 4px 14px rgba(155,93,229,0.3); }
|
||
.btn-launch:disabled { opacity: 0.5; cursor: default; transform: none; box-shadow: none; }
|
||
|
||
/* ── active question display ── */
|
||
.lq-active-wrap {
|
||
background: #fff; border: 1.5px solid rgba(155,93,229,0.25);
|
||
border-radius: 20px; padding: 22px 24px;
|
||
box-shadow: 0 4px 16px rgba(155,93,229,0.08); margin-bottom: 20px;
|
||
}
|
||
.lq-active-label {
|
||
font-size: 0.68rem; font-weight: 700; color: var(--violet);
|
||
text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 10px;
|
||
display: flex; align-items: center; gap: 6px;
|
||
}
|
||
.lq-active-text {
|
||
font-size: 0.96rem; font-weight: 700; color: #0F172A;
|
||
line-height: 1.55; margin-bottom: 16px;
|
||
}
|
||
.lq-active-options { display: flex; flex-direction: column; gap: 6px; margin-bottom: 16px; }
|
||
.lq-active-opt {
|
||
padding: 8px 14px; border-radius: 10px;
|
||
border: 1.5px solid rgba(15,23,42,0.08);
|
||
font-size: 0.82rem; font-weight: 500; color: #3D4F6B;
|
||
display: flex; align-items: center; gap: 10px;
|
||
}
|
||
.lq-active-opt.correct {
|
||
border-color: rgba(6,214,160,0.4); background: rgba(6,214,160,0.06);
|
||
color: #059652; font-weight: 700;
|
||
}
|
||
.lq-opt-letter {
|
||
width: 22px; height: 22px; border-radius: 6px;
|
||
background: rgba(15,23,42,0.07); flex-shrink: 0;
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-size: 0.72rem; font-weight: 800; color: #8898AA;
|
||
}
|
||
.lq-active-opt.correct .lq-opt-letter { background: #06D6A0; color: #fff; }
|
||
|
||
/* answer counter */
|
||
.lq-counter {
|
||
display: flex; align-items: center; gap: 16px;
|
||
padding: 14px 18px; background: rgba(155,93,229,0.05);
|
||
border-radius: 14px; margin-bottom: 14px;
|
||
}
|
||
.lq-counter-val {
|
||
font-family: 'Unbounded', sans-serif; font-size: 1.4rem; font-weight: 900;
|
||
color: var(--violet);
|
||
}
|
||
.lq-counter-label { font-size: 0.78rem; color: #8898AA; font-weight: 600; }
|
||
.lq-counter-bar-wrap {
|
||
flex: 1; height: 8px; background: rgba(155,93,229,0.1); border-radius: 999px; overflow: hidden;
|
||
}
|
||
.lq-counter-bar {
|
||
height: 100%; background: linear-gradient(90deg, #9B5DE5, #06D6E0);
|
||
border-radius: 999px; transition: width 0.4s ease;
|
||
}
|
||
|
||
.btn-show-results {
|
||
padding: 10px 24px; border: none; border-radius: 999px;
|
||
background: linear-gradient(135deg, #06D6E0, #9B5DE5);
|
||
font-family: 'Unbounded', sans-serif; font-size: 0.78rem; font-weight: 800;
|
||
color: #fff; cursor: pointer; transition: transform 0.12s, box-shadow 0.12s;
|
||
display: flex; align-items: center; gap: 7px;
|
||
}
|
||
.btn-show-results:hover { transform: translateY(-1px); box-shadow: 0 6px 20px rgba(155,93,229,0.3); }
|
||
|
||
/* ── results chart ── */
|
||
.lq-results-wrap {
|
||
background: #fff; border: 1.5px solid rgba(15,23,42,0.07);
|
||
border-radius: 20px; padding: 20px 22px; margin-top: 14px;
|
||
}
|
||
.lq-results-title {
|
||
font-family: 'Unbounded', sans-serif; font-size: 0.78rem; font-weight: 800;
|
||
color: #0F172A; margin-bottom: 12px; display: flex; align-items: center; gap: 8px;
|
||
}
|
||
.result-bars { display: flex; flex-direction: column; gap: 10px; }
|
||
.result-bar-row { display: flex; align-items: center; gap: 10px; }
|
||
.result-bar-label {
|
||
min-width: 120px; font-size: 0.78rem; font-weight: 600; color: #3D4F6B;
|
||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||
display: flex; align-items: center; gap: 6px;
|
||
}
|
||
.result-bar-label.correct-lbl { color: #059652; font-weight: 700; }
|
||
.rb-correct-marker { color: #06D6A0; }
|
||
.result-bar-track { flex: 1; height: 18px; background: rgba(15,23,42,0.05); border-radius: 999px; overflow: hidden; position: relative; }
|
||
.result-bar-fill {
|
||
height: 100%; border-radius: 999px; transition: width 0.5s ease;
|
||
background: rgba(155,93,229,0.35);
|
||
}
|
||
.result-bar-fill.correct-fill { background: linear-gradient(90deg, #06D6A0, #06D6E0); }
|
||
.result-bar-count {
|
||
min-width: 36px; text-align: right;
|
||
font-family: 'Unbounded', sans-serif; font-size: 0.72rem; font-weight: 800; color: #0F172A;
|
||
}
|
||
|
||
/* no session state */
|
||
.lq-no-session {
|
||
text-align: center; padding: 80px 20px; color: #8898AA;
|
||
}
|
||
.lq-no-session-icon { margin-bottom: 14px; opacity: 0.2; }
|
||
|
||
/* badges */
|
||
.badge { display: inline-flex; align-items: center; padding: 2px 8px; border-radius: 999px; font-size: 0.66rem; font-weight: 700; }
|
||
.badge-diff-1 { background: rgba(6,214,160,0.12); color: #059652; }
|
||
.badge-diff-2 { background: rgba(255,179,71,0.15); color: #B8860B; }
|
||
.badge-diff-3 { background: rgba(239,71,111,0.12); color: #EF476F; }
|
||
.badge-type { background: rgba(6,214,224,0.12); color: #0891B2; }
|
||
|
||
/* ── nav ── */
|
||
.nav-active {
|
||
background: rgba(155,93,229,0.08) !important;
|
||
border-color: var(--violet) !important;
|
||
color: var(--violet) !important;
|
||
cursor: default; pointer-events: none;
|
||
}
|
||
.notif-drop-header { display: flex; align-items: center; justify-content: space-between; padding: 14px 16px 10px; border-bottom: 1px solid var(--border); }
|
||
.notif-drop-title { font-family: 'Unbounded', sans-serif; font-size: 0.78rem; font-weight: 800; }
|
||
.notif-read-all { background: none; border: none; font-size: 0.74rem; color: var(--violet); cursor: pointer; font-family: 'Manrope', sans-serif; font-weight: 600; }
|
||
.notif-item { display: flex; gap: 10px; padding: 11px 16px; border-bottom: 1px solid var(--border); cursor: pointer; transition: background var(--tr); text-decoration: none; color: inherit; }
|
||
.notif-item:last-child { border-bottom: none; }
|
||
.notif-item:hover { background: rgba(155,93,229,0.04); }
|
||
.notif-item.unread { background: rgba(155,93,229,0.05); }
|
||
.notif-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--violet); flex-shrink: 0; margin-top: 5px; }
|
||
.notif-dot.read { background: transparent; border: 1.5px solid var(--border-h); }
|
||
.notif-msg { font-size: 0.80rem; line-height: 1.45; flex: 1; }
|
||
.notif-time { font-size: 0.70rem; color: var(--text-3); margin-top: 2px; }
|
||
.notif-empty { padding: 28px 16px; text-align: center; color: var(--text-3); font-size: 0.84rem; }
|
||
|
||
@media (max-width: 768px) {
|
||
.lq-body { flex-direction: column; }
|
||
.lq-left { width: 100%; max-height: 220px; border-right: none; border-bottom: 1.5px solid rgba(15,23,42,0.08); }
|
||
.lq-right { padding: 14px 12px 60px; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="app-layout">
|
||
<aside class="sidebar" id="app-sidebar"></aside>
|
||
<div class="notif-drop" id="notif-drop"></div>
|
||
<div class="sb-content">
|
||
|
||
<!-- Header -->
|
||
<div class="lq-header">
|
||
<div class="lq-header-dots"></div>
|
||
<div class="lq-header-inner">
|
||
<div class="lq-title"><i data-lucide="radio" style="width:22px;height:22px;opacity:0.5"></i> Live-квиз</div>
|
||
<div id="session-header-chip" style="display:none" class="lq-session-chip">
|
||
<div class="dot"></div>
|
||
<span id="session-header-label">Сессия активна</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Body -->
|
||
<div class="lq-body">
|
||
|
||
<!-- Left: class selector + session control -->
|
||
<div class="lq-left">
|
||
<div class="lq-panel-head">
|
||
<i data-lucide="graduation-cap" style="width:14px;height:14px;opacity:0.5"></i>
|
||
Выберите класс
|
||
</div>
|
||
<div class="class-list" id="class-list">
|
||
<div style="padding:30px;text-align:center;color:#8898AA;font-size:0.82rem">
|
||
<div class="spinner" style="margin:0 auto 10px"></div>
|
||
Загрузка классов…
|
||
</div>
|
||
</div>
|
||
<div class="lq-session-area" id="session-area">
|
||
<div id="active-status-wrap" style="display:none">
|
||
<div class="active-status">
|
||
<div class="as-label">Статус сессии</div>
|
||
<div class="as-val"><div class="as-dot"></div> <span id="as-students-text">Активна</span></div>
|
||
</div>
|
||
</div>
|
||
<button class="btn-start" id="btn-start" onclick="startSession()" disabled>
|
||
<i data-lucide="play" style="width:14px;height:14px"></i>
|
||
Начать сессию
|
||
</button>
|
||
<button class="btn-end" id="btn-end" onclick="endSession()" style="display:none">
|
||
<i data-lucide="square" style="width:14px;height:14px"></i>
|
||
Завершить сессию
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Right: question launcher + active question -->
|
||
<div class="lq-right">
|
||
|
||
<!-- No session state -->
|
||
<div id="no-session-state">
|
||
<div class="lq-no-session">
|
||
<div class="lq-no-session-icon"><i data-lucide="radio" style="width:64px;height:64px"></i></div>
|
||
<div style="font-family:'Unbounded',sans-serif;font-weight:800;font-size:1rem;color:#0F172A;margin-bottom:8px">Нет активной сессии</div>
|
||
<div style="font-size:0.84rem">Выберите класс и нажмите «Начать сессию»,<br>чтобы запустить Live-квиз</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Session active state -->
|
||
<div id="session-state" style="display:none">
|
||
|
||
<!-- Active question -->
|
||
<div id="active-q-wrap" style="display:none">
|
||
<div class="lq-section-title" style="margin-bottom:10px">
|
||
<i data-lucide="zap" style="width:14px;height:14px;color:#FFB347"></i>
|
||
Активный вопрос
|
||
</div>
|
||
<div class="lq-active-wrap" id="active-q-card">
|
||
<!-- filled by JS -->
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Question search & list -->
|
||
<div class="lq-section-title">
|
||
<i data-lucide="list" style="width:14px;height:14px;opacity:0.5"></i>
|
||
Вопросы для запуска
|
||
</div>
|
||
<div class="lq-search-wrap">
|
||
<i data-lucide="search" class="lq-search-icon"></i>
|
||
<input class="lq-search" id="q-search" type="text" placeholder="Поиск вопросов…" />
|
||
</div>
|
||
<div class="lq-filter-row">
|
||
<select class="lq-filter-select" id="topic-filter" onchange="onTopicFilter()">
|
||
<option value="">Все темы</option>
|
||
</select>
|
||
<select class="lq-filter-select" id="diff-filter" onchange="onDiffFilter()" style="max-width:130px">
|
||
<option value="">Любой уровень</option>
|
||
<option value="1">Лёгкий</option>
|
||
<option value="2">Средний</option>
|
||
<option value="3">Сложный</option>
|
||
</select>
|
||
</div>
|
||
<div class="lq-q-count" id="q-count" style="display:none"></div>
|
||
<div class="lq-q-list" id="q-list">
|
||
<div style="padding:30px;text-align:center;color:#8898AA;font-size:0.82rem">
|
||
<div class="spinner" style="margin:0 auto 10px"></div>
|
||
Загрузка вопросов…
|
||
</div>
|
||
</div>
|
||
<button class="btn-load-more" id="btn-load-more" style="display:none" onclick="loadMoreQuestions()">
|
||
Загрузить ещё
|
||
</button>
|
||
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="/js/api.js"></script>
|
||
<script src="/js/sound.js"></script>
|
||
<script src="/js/sidebar.js"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"></script>
|
||
<script>
|
||
if (!LS.requireAuth()) throw new Error();
|
||
|
||
const user = LS.getUser();
|
||
document.getElementById('nav-user').textContent = user?.name || user?.email || '';
|
||
document.getElementById('nav-avatar').textContent =
|
||
(user?.name || 'LS').split(' ').slice(0, 2).map(w => w[0]?.toUpperCase() || '').join('') || 'LS';
|
||
|
||
const isTeacher = ['admin', 'teacher'].includes(user?.role);
|
||
if (!isTeacher) { location.href = '/dashboard'; throw new Error(); }
|
||
if (user?.role === 'admin') document.getElementById('btn-admin').style.display = '';
|
||
|
||
/* ── helpers ── */
|
||
function fmtTime(s) {
|
||
const d = new Date(s && s.includes('T') ? s : (s || '').replace(' ', 'T') + 'Z');
|
||
const diff = Date.now() - d.getTime();
|
||
if (diff < 60000) return 'только что';
|
||
if (diff < 3600000) return Math.floor(diff / 60000) + ' мин назад';
|
||
return d.toLocaleDateString('ru', { day: 'numeric', month: 'short' });
|
||
}
|
||
const TYPE_LABELS = {
|
||
single: 'Один ответ', multi: 'Несколько', true_false: 'Да/Нет',
|
||
short_answer: 'Краткий ответ', matching: 'Соответствие',
|
||
'fill-blank': 'Заполни пробел', ordering: 'Порядок',
|
||
};
|
||
|
||
/* ── math rendering ── */
|
||
const MATH_DELIMS = [
|
||
{ left: '\\(', right: '\\)', display: false },
|
||
{ left: '\\[', right: '\\]', display: true },
|
||
{ left: '$', right: '$', display: false },
|
||
];
|
||
function mathHtml(text) {
|
||
if (!text) return '';
|
||
const tmp = document.createElement('span');
|
||
tmp.textContent = text;
|
||
if (window.renderMathInElement) {
|
||
try { renderMathInElement(tmp, { delimiters: MATH_DELIMS, throwOnError: false }); } catch {}
|
||
}
|
||
return tmp.innerHTML;
|
||
}
|
||
|
||
/* ── sidebar ── */
|
||
function toggleSidebar() {
|
||
const layout = document.querySelector('.app-layout');
|
||
const collapsed = layout.classList.toggle('sb-collapsed');
|
||
localStorage.setItem('ls_sb_collapsed', collapsed ? '1' : '');
|
||
lucide.createIcons();
|
||
}
|
||
if (localStorage.getItem('ls_sb_collapsed'))
|
||
document.querySelector('.app-layout').classList.add('sb-collapsed');
|
||
|
||
/* ── notifications ── */
|
||
function toggleNotifDrop() {
|
||
const btn = document.getElementById('notif-btn');
|
||
const drop = document.getElementById('notif-drop');
|
||
const r = btn.getBoundingClientRect();
|
||
drop.style.left = (r.right + 8) + 'px';
|
||
drop.style.top = r.top + 'px';
|
||
if (drop.classList.toggle('open')) loadNotifs();
|
||
}
|
||
async function loadNotifs() {
|
||
const drop = document.getElementById('notif-drop');
|
||
drop.innerHTML = '<div class="notif-drop-header"><span class="notif-drop-title">Уведомления</span><button class="notif-read-all" onclick="readAllNotifs()">Прочитать все</button></div><div class="notif-empty">Загрузка…</div>';
|
||
try {
|
||
const data = await LS.api('/api/notifications?limit=20');
|
||
const items = data.items || [];
|
||
const badge = document.getElementById('notif-badge');
|
||
const unread = items.filter(n => !n.is_read).length;
|
||
badge.textContent = unread; badge.style.display = unread ? '' : 'none';
|
||
if (!items.length) { drop.querySelector('.notif-empty').textContent = 'Нет уведомлений'; return; }
|
||
drop.innerHTML = '<div class="notif-drop-header"><span class="notif-drop-title">Уведомления</span><button class="notif-read-all" onclick="readAllNotifs()">Прочитать все</button></div>' +
|
||
items.map(n => `<a class="notif-item${n.is_read ? '' : ' unread'}" href="${n.link || '#'}" onclick="markRead(${n.id})">
|
||
<div class="notif-dot${n.is_read ? ' read' : ''}"></div>
|
||
<div><div class="notif-msg">${esc(n.message)}</div><div class="notif-time">${fmtTime(n.created_at)}</div></div>
|
||
</a>`).join('');
|
||
} catch {}
|
||
}
|
||
async function markRead(id) { try { await LS.api('/api/notifications/' + id + '/read', { method: 'POST' }); } catch {} }
|
||
async function readAllNotifs() { try { await LS.api('/api/notifications/read-all', { method: 'POST' }); loadNotifs(); } catch {} }
|
||
document.addEventListener('click', e => {
|
||
const drop = document.getElementById('notif-drop');
|
||
if (drop.classList.contains('open') && !drop.contains(e.target) && !document.getElementById('notif-btn').contains(e.target))
|
||
drop.classList.remove('open');
|
||
});
|
||
|
||
/* ── state ── */
|
||
let selectedClass = null;
|
||
let activeSession = null;
|
||
let currentQuestion = null;
|
||
let memberCount = 0;
|
||
let answerCount = 0;
|
||
let sseSource = null;
|
||
let allQuestions = [];
|
||
let searchTimeout = null;
|
||
let _topicFilter = '';
|
||
let _diffFilter = '';
|
||
let _qPage = 0;
|
||
let _totalQ = 0;
|
||
const Q_LIMIT = 30;
|
||
|
||
/* ── load classes ── */
|
||
async function loadClasses() {
|
||
try {
|
||
const classes = await LS.api('/api/classes');
|
||
const list = document.getElementById('class-list');
|
||
if (!(classes || []).length) {
|
||
list.innerHTML = '<div style="padding:30px;text-align:center;color:#8898AA;font-size:0.82rem">Нет доступных классов</div>';
|
||
lucide.createIcons();
|
||
return;
|
||
}
|
||
let html = '';
|
||
(classes || []).forEach(c => {
|
||
const initials = (c.name || '?').split(' ').slice(0, 2).map(w => w[0]?.toUpperCase() || '').join('') || '?';
|
||
html += `<div class="class-card" id="cc-${c.id}" onclick="selectClass(${c.id}, '${esc(c.name)}', ${c.members_count || 0})">
|
||
<div class="class-card-icon">${initials}</div>
|
||
<div>
|
||
<div class="class-card-name">${esc(c.name)}</div>
|
||
<div class="class-card-meta">${c.members_count || 0} учеников</div>
|
||
</div>
|
||
</div>`;
|
||
});
|
||
list.innerHTML = html;
|
||
lucide.createIcons();
|
||
} catch {
|
||
document.getElementById('class-list').innerHTML = '<div style="padding:20px;text-align:center;color:#8898AA;font-size:0.82rem">Ошибка загрузки</div>';
|
||
}
|
||
}
|
||
|
||
async function selectClass(id, name, members) {
|
||
if (activeSession) {
|
||
if (!await LS.confirm('Выбрать другой класс?', { title: 'Завершить текущую сессию?', confirmText: 'Завершить', danger: true })) return;
|
||
endSession(true);
|
||
}
|
||
selectedClass = { id, name, members };
|
||
memberCount = members;
|
||
|
||
document.querySelectorAll('.class-card').forEach(el => el.classList.remove('active'));
|
||
const card = document.getElementById('cc-' + id);
|
||
if (card) card.classList.add('active');
|
||
|
||
document.getElementById('btn-start').disabled = false;
|
||
lucide.createIcons();
|
||
}
|
||
|
||
/* ── start session ── */
|
||
async function startSession() {
|
||
if (!selectedClass || activeSession) return;
|
||
const btn = document.getElementById('btn-start');
|
||
btn.disabled = true;
|
||
btn.innerHTML = '<div class="spinner" style="width:16px;height:16px;margin:0"></div> Создаём…';
|
||
try {
|
||
const session = await LS.api('/api/live', {
|
||
method: 'POST',
|
||
body: JSON.stringify({ class_id: selectedClass.id }),
|
||
});
|
||
activeSession = session;
|
||
onSessionActive();
|
||
loadQuestions();
|
||
startSSE();
|
||
} catch (e) {
|
||
LS.toast(e.message || 'Ошибка создания сессии', 'error');
|
||
btn.disabled = false;
|
||
btn.innerHTML = '<i data-lucide="play" style="width:14px;height:14px"></i> Начать сессию';
|
||
lucide.createIcons();
|
||
}
|
||
}
|
||
|
||
function onSessionActive() {
|
||
document.getElementById('no-session-state').style.display = 'none';
|
||
document.getElementById('session-state').style.display = '';
|
||
document.getElementById('active-status-wrap').style.display = '';
|
||
document.getElementById('btn-start').style.display = 'none';
|
||
document.getElementById('btn-end').style.display = '';
|
||
document.getElementById('session-header-chip').style.display = '';
|
||
document.getElementById('session-header-label').innerHTML =
|
||
'<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="8" fill="currentColor" stroke="none"/></svg> Активна · ' + selectedClass.name;
|
||
updateStudentCounter(0);
|
||
loadTopics();
|
||
lucide.createIcons();
|
||
}
|
||
|
||
async function loadTopics() {
|
||
try {
|
||
const topics = await LS.api('/api/topics');
|
||
const sel = document.getElementById('topic-filter');
|
||
sel.innerHTML = '<option value="">Все темы</option>';
|
||
(topics || []).forEach(t => {
|
||
const opt = document.createElement('option');
|
||
opt.value = t.id;
|
||
opt.textContent = t.name;
|
||
sel.appendChild(opt);
|
||
});
|
||
} catch {}
|
||
}
|
||
|
||
function updateStudentCounter(count) {
|
||
answerCount = count;
|
||
document.getElementById('as-students-text').textContent =
|
||
'Ответили: ' + count + ' / ' + memberCount;
|
||
}
|
||
|
||
/* ── end session ── */
|
||
async function endSession(silent = false) {
|
||
if (!activeSession) return;
|
||
if (!silent && !await LS.confirm('Все участники увидят, что сессия завершена.', { title: 'Завершить Live-квиз?', confirmText: 'Завершить', danger: true })) return;
|
||
const id = activeSession.id;
|
||
activeSession = null;
|
||
currentQuestion = null;
|
||
if (sseSource) { sseSource.close(); sseSource = null; }
|
||
|
||
document.getElementById('no-session-state').style.display = '';
|
||
document.getElementById('session-state').style.display = 'none';
|
||
document.getElementById('active-q-wrap').style.display = 'none';
|
||
document.getElementById('active-status-wrap').style.display = 'none';
|
||
document.getElementById('btn-start').style.display = '';
|
||
document.getElementById('btn-end').style.display = 'none';
|
||
document.getElementById('session-header-chip').style.display = 'none';
|
||
const btn = document.getElementById('btn-start');
|
||
btn.disabled = !selectedClass;
|
||
btn.innerHTML = '<i data-lucide="play" style="width:14px;height:14px"></i> Начать сессию';
|
||
lucide.createIcons();
|
||
|
||
try { await LS.api('/api/live/' + id, { method: 'DELETE' }); } catch {}
|
||
}
|
||
|
||
/* ── SSE for real-time answer count ── */
|
||
function startSSE() {
|
||
const token = localStorage.getItem('ls_token');
|
||
if (!token) return;
|
||
try {
|
||
sseSource = new EventSource('/api/notifications/stream?token=' + encodeURIComponent(token));
|
||
sseSource.onmessage = (ev) => {
|
||
try {
|
||
const msg = JSON.parse(ev.data);
|
||
if (msg.type === 'live_answer_count' && activeSession && msg.liveId === activeSession.id) {
|
||
updateStudentCounter(msg.count || 0);
|
||
// update counter in active-q-card if visible
|
||
const counterEl = document.getElementById('lq-answer-count');
|
||
if (counterEl) counterEl.textContent = (msg.count || 0) + ' / ' + memberCount;
|
||
const barEl = document.getElementById('lq-answer-bar');
|
||
if (barEl) {
|
||
const pct = memberCount > 0 ? Math.round(((msg.count || 0) / memberCount) * 100) : 0;
|
||
barEl.style.width = pct + '%';
|
||
}
|
||
}
|
||
} catch {}
|
||
};
|
||
sseSource.onerror = () => {
|
||
sseSource.close();
|
||
sseSource = null;
|
||
// retry after 5s if session still active
|
||
if (activeSession) setTimeout(startSSE, 5000);
|
||
};
|
||
} catch {}
|
||
}
|
||
|
||
/* ── load questions ── */
|
||
async function loadQuestions(reset = true) {
|
||
if (reset) { _qPage = 0; allQuestions = []; }
|
||
if (reset) document.getElementById('q-list').innerHTML = '<div style="padding:20px;text-align:center;color:#8898AA"><div class="spinner" style="margin:0 auto 10px"></div> Загрузка…</div>';
|
||
const params = new URLSearchParams({ limit: Q_LIMIT, offset: _qPage * Q_LIMIT });
|
||
if (_topicFilter) params.set('topic_id', _topicFilter);
|
||
if (_diffFilter) params.set('difficulty', _diffFilter);
|
||
const sq = document.getElementById('q-search')?.value.trim();
|
||
if (sq) params.set('search', sq);
|
||
try {
|
||
const data = await LS.api('/api/questions?' + params.toString());
|
||
const rows = data.rows || [];
|
||
_totalQ = data.total ?? (reset ? rows.length : allQuestions.length + rows.length);
|
||
allQuestions = reset ? rows : [...allQuestions, ...rows];
|
||
renderQuestionList();
|
||
const btnMore = document.getElementById('btn-load-more');
|
||
const countEl = document.getElementById('q-count');
|
||
if (btnMore) btnMore.style.display = allQuestions.length < _totalQ ? '' : 'none';
|
||
if (countEl) { countEl.textContent = `Показано ${allQuestions.length} из ${_totalQ}`; countEl.style.display = ''; }
|
||
} catch {
|
||
if (reset) document.getElementById('q-list').innerHTML = '<div style="padding:20px;text-align:center;color:#8898AA;font-size:0.82rem">Ошибка загрузки вопросов</div>';
|
||
}
|
||
}
|
||
|
||
async function loadMoreQuestions() {
|
||
const btn = document.getElementById('btn-load-more');
|
||
if (btn) { btn.disabled = true; btn.textContent = 'Загрузка…'; }
|
||
_qPage++;
|
||
await loadQuestions(false);
|
||
if (btn) { btn.disabled = false; btn.textContent = 'Загрузить ещё'; }
|
||
}
|
||
|
||
function onTopicFilter() { _topicFilter = document.getElementById('topic-filter').value; loadQuestions(true); }
|
||
function onDiffFilter() { _diffFilter = document.getElementById('diff-filter').value; loadQuestions(true); }
|
||
|
||
function renderQuestionList() {
|
||
const list = document.getElementById('q-list');
|
||
if (!allQuestions.length) {
|
||
list.innerHTML = '<div style="padding:30px;text-align:center;color:#8898AA;font-size:0.82rem">Вопросов не найдено</div>';
|
||
lucide.createIcons();
|
||
return;
|
||
}
|
||
let html = '';
|
||
allQuestions.forEach(q => {
|
||
const diffCls = 'badge-diff-' + (q.difficulty || 1);
|
||
const diffLabel = q.difficulty === 1 ? 'Лёгкий' : q.difficulty === 2 ? 'Средний' : 'Сложный';
|
||
const typeLabel = TYPE_LABELS[q.type] || q.type || '';
|
||
const isLaunched = currentQuestion && currentQuestion.id === q.id;
|
||
html += `<div class="lq-q-card${isLaunched ? ' launched' : ''}">
|
||
<div class="lq-q-body">
|
||
<div class="lq-q-text" data-text="${esc(q.text)}"></div>
|
||
<div class="lq-q-meta">
|
||
<span class="badge ${diffCls}">${diffLabel}</span>
|
||
${typeLabel ? `<span class="badge badge-type">${esc(typeLabel)}</span>` : ''}
|
||
${q.topic ? `<span>${esc(q.topic)}</span>` : ''}
|
||
</div>
|
||
</div>
|
||
<button class="btn-launch" onclick="launchQuestion(${q.id})" ${isLaunched ? 'disabled' : ''}>
|
||
${isLaunched ? '<i data-lucide="check" style="width:13px;height:13px"></i> Запущен' : '<i data-lucide="zap" style="width:13px;height:13px"></i> Запустить'}
|
||
</button>
|
||
</div>`;
|
||
});
|
||
list.innerHTML = html;
|
||
list.querySelectorAll('.lq-q-text[data-text]').forEach(el => { el.innerHTML = mathHtml(el.dataset.text); });
|
||
lucide.createIcons();
|
||
}
|
||
|
||
/* search questions — server-side */
|
||
document.getElementById('q-search').addEventListener('input', () => {
|
||
clearTimeout(searchTimeout);
|
||
searchTimeout = setTimeout(() => loadQuestions(true), 350);
|
||
});
|
||
|
||
/* ── launch question ── */
|
||
async function launchQuestion(questionId) {
|
||
if (!activeSession) return;
|
||
try {
|
||
const resp = await LS.api('/api/live/' + activeSession.id + '/question', {
|
||
method: 'PUT',
|
||
body: JSON.stringify({ question_id: questionId }),
|
||
});
|
||
currentQuestion = resp.question || allQuestions.find(x => x.id === questionId) || { id: questionId };
|
||
answerCount = 0;
|
||
updateStudentCounter(0);
|
||
renderActiveQuestion(currentQuestion);
|
||
renderQuestionList();
|
||
if (window.LS && LS.sfx) LS.sfx.play('quiz_start');
|
||
} catch (e) {
|
||
LS.toast(e.message || 'Ошибка запуска вопроса', 'error');
|
||
}
|
||
}
|
||
|
||
function renderActiveQuestion(q) {
|
||
const wrap = document.getElementById('active-q-wrap');
|
||
const card = document.getElementById('active-q-card');
|
||
wrap.style.display = '';
|
||
|
||
const diffCls = 'badge-diff-' + (q.difficulty || 1);
|
||
const diffLabel = q.difficulty === 1 ? 'Лёгкий' : q.difficulty === 2 ? 'Средний' : 'Сложный';
|
||
const typeLabel = TYPE_LABELS[q.type] || q.type || '';
|
||
const pct = memberCount > 0 ? Math.round((answerCount / memberCount) * 100) : 0;
|
||
|
||
let optionsHtml = '';
|
||
if ((q.options || []).length) {
|
||
optionsHtml = '<div class="lq-active-options">';
|
||
q.options.forEach((opt, idx) => {
|
||
const letter = String.fromCharCode(65 + idx);
|
||
optionsHtml += `<div class="lq-active-opt${opt.is_correct ? ' correct' : ''}">
|
||
<div class="lq-opt-letter">${opt.is_correct ? '<svg class="ic" viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg>' : letter}</div>
|
||
<span data-text="${esc(opt.text)}"></span>
|
||
</div>`;
|
||
});
|
||
optionsHtml += '</div>';
|
||
}
|
||
|
||
card.innerHTML = `
|
||
<div class="lq-active-label">
|
||
<div class="dot" style="width:7px;height:7px;border-radius:50%;background:#FFB347;animation:pulse-dot 1.5s ease infinite"></div>
|
||
В эфире · <span class="badge ${diffCls}">${diffLabel}</span>
|
||
${typeLabel ? `<span class="badge badge-type">${esc(typeLabel)}</span>` : ''}
|
||
</div>
|
||
<div class="lq-active-text" data-text="${esc(q.text)}"></div>
|
||
${optionsHtml}
|
||
<div class="lq-counter">
|
||
<div>
|
||
<div class="lq-counter-val" id="lq-answer-count">${answerCount} / ${memberCount}</div>
|
||
<div class="lq-counter-label">студентов ответили</div>
|
||
</div>
|
||
<div class="lq-counter-bar-wrap">
|
||
<div class="lq-counter-bar" id="lq-answer-bar" style="width:${pct}%"></div>
|
||
</div>
|
||
</div>
|
||
<button class="btn-show-results" onclick="showResults()">
|
||
<i data-lucide="bar-chart-2" style="width:14px;height:14px"></i>
|
||
Показать результаты
|
||
</button>
|
||
<div id="results-area"></div>
|
||
`;
|
||
card.querySelectorAll('[data-text]').forEach(el => { el.innerHTML = mathHtml(el.dataset.text); });
|
||
lucide.createIcons();
|
||
wrap.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||
}
|
||
|
||
/* ── show results ── */
|
||
async function showResults() {
|
||
if (!activeSession || !currentQuestion) return;
|
||
const resultsArea = document.getElementById('results-area');
|
||
if (!resultsArea) return;
|
||
resultsArea.innerHTML = '<div class="spinner" style="margin:20px auto"></div>';
|
||
try {
|
||
const data = await LS.api('/api/live/' + activeSession.id + '/results');
|
||
renderResults(data, resultsArea);
|
||
if (window.LS && LS.sfx) LS.sfx.play('quiz_end');
|
||
} catch (e) {
|
||
resultsArea.innerHTML = `<div style="padding:20px;text-align:center;color:#8898AA;font-size:0.84rem">${esc(e.message || 'Ошибка загрузки результатов')}</div>`;
|
||
}
|
||
}
|
||
|
||
function renderResults(data, container) {
|
||
const opts = data.options || [];
|
||
const q = data.question || {};
|
||
const stats = data.stats || {};
|
||
const total = stats.total || 0;
|
||
const correct= stats.correct|| 0;
|
||
const maxCount = Math.max(...opts.map(o => o.chosen_count || 0), 1);
|
||
|
||
if (!opts.length) {
|
||
container.innerHTML = '<div style="padding:16px;text-align:center;color:#8898AA;font-size:0.84rem">Нет данных о вариантах ответа</div>';
|
||
return;
|
||
}
|
||
|
||
const pctCorrect = total > 0 ? Math.round(correct / total * 100) : 0;
|
||
const pctWrong = total > 0 ? 100 - pctCorrect : 0;
|
||
|
||
let html = `<div class="lq-results-wrap">
|
||
<div class="lq-results-title"><i data-lucide="bar-chart-horizontal" style="width:14px;height:14px;opacity:0.5"></i> Результаты</div>
|
||
<div class="lq-result-stats">
|
||
<div class="lq-result-stat">
|
||
<div class="lq-result-stat-val">${total}</div>
|
||
<div class="lq-result-stat-lbl">Ответов</div>
|
||
</div>
|
||
<div class="lq-result-stat rs-correct">
|
||
<div class="lq-result-stat-val">${pctCorrect}%</div>
|
||
<div class="lq-result-stat-lbl">Верно</div>
|
||
</div>
|
||
<div class="lq-result-stat rs-wrong">
|
||
<div class="lq-result-stat-val">${pctWrong}%</div>
|
||
<div class="lq-result-stat-lbl">Неверно</div>
|
||
</div>
|
||
</div>
|
||
<div class="result-bars">`;
|
||
|
||
opts.forEach((opt, idx) => {
|
||
const letter = String.fromCharCode(65 + idx);
|
||
const count = opt.chosen_count || 0;
|
||
const pct = Math.round(count / maxCount * 100);
|
||
const isCorrect = opt.is_correct;
|
||
html += `<div class="result-bar-row">
|
||
<div class="result-bar-label${isCorrect ? ' correct-lbl' : ''}">
|
||
${isCorrect ? '<span class="rb-correct-marker"><svg class="ic" viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg></span>' : letter + '.'}
|
||
<span class="rb-opt-text" data-text="${esc(opt.text)}" style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100px"></span>
|
||
</div>
|
||
<div class="result-bar-track">
|
||
<div class="result-bar-fill${isCorrect ? ' correct-fill' : ''}" style="width:${pct}%"></div>
|
||
</div>
|
||
<div class="result-bar-count">${count}</div>
|
||
</div>`;
|
||
});
|
||
|
||
html += '</div>';
|
||
if (q.explanation) {
|
||
html += `<div class="lq-explanation">
|
||
<div class="lq-explanation-label">Объяснение</div>
|
||
<div class="lq-exp-text" data-text="${esc(q.explanation)}"></div>
|
||
</div>`;
|
||
}
|
||
html += '</div>';
|
||
container.innerHTML = html;
|
||
container.querySelectorAll('[data-text]').forEach(el => { el.innerHTML = mathHtml(el.dataset.text); });
|
||
lucide.createIcons();
|
||
}
|
||
|
||
lucide.createIcons();
|
||
loadClasses();
|
||
</script>
|
||
<script src="/js/notifications.js"></script>
|
||
<script src="/js/search.js"></script>
|
||
<script src="/js/mobile.js"></script>
|
||
</body>
|
||
</html>
|