feat: WebSocket real-time + rAF render gate + guest board + screen picker
Classroom performance: - WebSocket server (ws-server.js) for low-latency cursor & stroke preview Replaces HTTP POST per event → eliminates per-message auth overhead Session member cache (30s TTL) avoids SQLite query per WS message Fallback to HTTP POST when WS not connected - Cursor throttle reduced 100ms → 33ms (~30fps) - Stroke preview throttle reduced 50ms → 20ms - whiteboard.js: render() is now rAF-gated (_doRender/_rafPending) Multiple render() calls within one frame collapse into one repaint document.hidden check — zero CPU when tab is in background visibilitychange listener restores canvas on tab focus Guest board: - guestClassroom.js route: public token-based read-only access - guest-board.html: name entry + read-only whiteboard + SSE - SSE: addGuestClient/removeGuestClient/emitToGuests Screen share picker: - Discord-style modal with tab switching (screen/window/tab) - Live video preview before confirming share - useExistingScreenStream() in ClassroomRTC Fullscreen exit overlay: - #cr-fs-exit-overlay button inside cr-board-wrap - Visible only via CSS :fullscreen selector (touchpad users) File sharing from library: - Teacher picks file from library, sends as styled card in chat - crDownloadLibraryFile() fetches with Bearer auth Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
<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; }
|
||||
|
||||
@@ -243,6 +244,7 @@
|
||||
<a href="/knowledge-map" class="sb-link"><i data-lucide="share-2" class="sb-icon"></i><span class="sb-lbl">Карта знаний</span></a>
|
||||
<a href="/red-book.html" class="sb-link"><i data-lucide="leaf" class="sb-icon"></i><span class="sb-lbl">Красная книга</span></a>
|
||||
<a href="/classroom" class="sb-link"><i data-lucide="presentation" class="sb-icon"></i><span class="sb-lbl">Онлайн-урок</span></a>
|
||||
<a href="/lesson-history" class="sb-link"><i data-lucide="archive" class="sb-icon"></i><span class="sb-lbl">Архив уроков</span></a>
|
||||
<div class="sb-divider"></div>
|
||||
<a href="/analytics" class="sb-link"><i data-lucide="bar-chart-2" class="sb-icon"></i><span class="sb-lbl">Аналитика</span></a>
|
||||
<a href="/question-bank" class="sb-link nav-active"><i data-lucide="database" class="sb-icon"></i><span class="sb-lbl">Банк вопросов</span></a>
|
||||
@@ -382,6 +384,8 @@
|
||||
</div>
|
||||
|
||||
<script src="/js/api.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();
|
||||
|
||||
@@ -409,6 +413,22 @@
|
||||
};
|
||||
const SUBJECT_NAMES = { bio: 'Биология', chem: 'Химия', math: 'Математика', phys: 'Физика', other: 'Другое' };
|
||||
|
||||
/* ── 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');
|
||||
@@ -570,7 +590,7 @@
|
||||
<span class="badge ${diffCls}">${diffLabel}</span>
|
||||
<span class="badge badge-type">${esc(typeLabel)}</span>
|
||||
</div>
|
||||
<div class="qc-text">${esc(q.text)}</div>
|
||||
<div class="qc-text">${mathHtml(q.text)}</div>
|
||||
<div class="qc-footer">
|
||||
<span class="qc-topic"><i data-lucide="tag" style="width:11px;height:11px"></i> ${esc(q.topic || SUBJECT_NAMES[q.subject_slug] || '—')}</span>
|
||||
${optsCount ? `<span class="qc-opts-count"><i data-lucide="list" style="width:11px;height:11px"></i> ${optsCount} вар.</span>` : ''}
|
||||
@@ -578,20 +598,20 @@
|
||||
|
||||
if (isExpanded) {
|
||||
html += `<div class="qc-preview">
|
||||
<div class="qc-preview-text">${esc(q.text)}</div>`;
|
||||
<div class="qc-preview-text">${mathHtml(q.text)}</div>`;
|
||||
if ((q.options || []).length) {
|
||||
html += '<div class="qc-options">';
|
||||
q.options.forEach((opt, idx) => {
|
||||
const letter = String.fromCharCode(65 + idx);
|
||||
html += `<div class="qc-option${opt.is_correct ? ' correct' : ''}">
|
||||
<div class="qc-option-marker">${opt.is_correct ? '<svg class="ic" viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg>' : letter}</div>
|
||||
<span>${esc(opt.text)}</span>
|
||||
<span>${mathHtml(opt.text)}</span>
|
||||
</div>`;
|
||||
});
|
||||
html += '</div>';
|
||||
}
|
||||
if (q.explanation) {
|
||||
html += `<div class="qc-explanation"><strong>Пояснение:</strong> ${esc(q.explanation)}</div>`;
|
||||
html += `<div class="qc-explanation"><strong>Пояснение:</strong> ${mathHtml(q.explanation)}</div>`;
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user