-
—
-
—
+
@@ -3177,41 +3218,49 @@
}
}
- /* ══ WIDGET: Continue reading (action card) ══════════════════════ */
+ /* ══ HERO: Reading card (continue or start) ══════════════════════ */
async function loadContinueWidget() {
- const card = document.getElementById('ac-continue');
- const cardsWrap = document.getElementById('action-cards');
+ const card = document.getElementById('hc-read');
if (!card) return;
try {
const data = await LS.api('/api/courses/continue');
- if (!data || !data.courseId) { card.style.display = 'none'; return; }
+ if (!data || !data.courseId) return; // оставляем дефолт «Начать чтение → Учебники»
const pct = data.lessonCount > 0 ? Math.round((data.doneCount || 0) / data.lessonCount * 100) : 0;
- const href = `/lesson?course=${data.courseId}&lesson=${data.lessonId}`;
- card.href = href;
- card.style.display = '';
- if (cardsWrap) cardsWrap.style.display = '';
- document.getElementById('ac-cont-title').textContent = data.courseTitle || 'Продолжить';
- document.getElementById('ac-cont-sub').textContent = `${data.lessonTitle || ''} · ${data.doneCount || 0}/${data.lessonCount || 0} уроков`;
- document.getElementById('ac-cont-pct').textContent = pct + '%';
- } catch { card.style.display = 'none'; }
+ card.href = `/lesson?course=${data.courseId}&lesson=${data.lessonId}`;
+ document.getElementById('hc-read-tag').textContent = 'Продолжить чтение';
+ document.getElementById('hc-read-title').textContent = data.courseTitle || 'Продолжить';
+ document.getElementById('hc-read-sub').textContent = data.lessonTitle || '';
+ document.getElementById('hc-read-meta').textContent = `${data.doneCount || 0} / ${data.lessonCount || 0} уроков`;
+ const progWrap = document.getElementById('hc-read-prog-wrap');
+ if (progWrap) { progWrap.style.display = ''; document.getElementById('hc-read-prog').style.width = pct + '%'; }
+ const pctEl = document.getElementById('hc-read-pct');
+ if (pctEl) { pctEl.style.display = ''; pctEl.textContent = pct + '%'; }
+ } catch { /* нет курса в процессе — карточка остаётся в дефолте */ }
}
- /* ══ WIDGET: Weak topics (action card) ════════════════════════════ */
- async function loadWeakWidget() {
- const card = document.getElementById('ac-weak');
- const cardsWrap = document.getElementById('action-cards');
+ /* ══ HERO: Lab of the day (deterministic daily pick) ═════════════ */
+ const LAB_OF_DAY = [
+ { key:'isoprocess', href:'/lab?sim=molphys', title:'Газовые законы', sub:'Давление, объём и температура газа.', subj:'Физика', time:'~10 мин', level:'средне', goal:'уравнение состояния газа' },
+ { key:'opticsbench', href:'/lab?sim=opticsbench', title:'Оптическая скамья', sub:'Собери систему линз и проследи ход лучей.', subj:'Физика', time:'~12 мин', level:'средне', goal:'построение изображения в линзе' },
+ { key:'circuit', href:'/lab?sim=circuit', title:'Электрическая цепь', sub:'Закон Ома: ток, напряжение и сопротивление.', subj:'Физика', time:'~8 мин', level:'легко', goal:'расчёт цепи по закону Ома' },
+ { key:'pendulum', href:'/lab?sim=pendulum', title:'Математический маятник', sub:'Период колебаний и зависимость от длины.', subj:'Физика', time:'~9 мин', level:'легко', goal:'формула периода маятника' },
+ { key:'waves', href:'/lab?sim=waves', title:'Волны и колебания', sub:'Длина волны, частота и стоячие волны.', subj:'Физика', time:'~11 мин', level:'средне', goal:'связь v = λf' },
+ { key:'stereo', href:'/lab?sim=stereo', title:'Стереометрия 3D', sub:'Сечения и объёмы пространственных фигур.', subj:'Геометрия', time:'~10 мин', level:'сложно', goal:'построение сечений' },
+ ];
+ function loadLabOfDay() {
+ const card = document.getElementById('hc-lab');
if (!card) return;
- try {
- const topics = await LS.getWeakTopics();
- if (!topics.length) { card.style.display = 'none'; return; }
- const t = topics[0];
- card.href = `/test-run?subject=${t.subject_slug}&mode=exam&count=15&topic=${t.topic_id}`;
- card.style.display = '';
- if (cardsWrap) cardsWrap.style.display = '';
- document.getElementById('ac-weak-title').textContent = t.topic;
- document.getElementById('ac-weak-sub').textContent = `${t.subject_name} · ${t.wrong} из ${t.total} неверно`;
- document.getElementById('ac-weak-pct').textContent = t.error_pct + '%';
- } catch { card.style.display = 'none'; }
+ const dayIdx = Math.floor(Date.now() / 86400000) % LAB_OF_DAY.length;
+ const lab = LAB_OF_DAY[dayIdx];
+ card.href = lab.href;
+ const bg = document.getElementById('hc-lab-bg');
+ if (bg && window.LabPreviews && LabPreviews[lab.key]) bg.innerHTML = LabPreviews[lab.key];
+ document.getElementById('hc-lab-title').textContent = lab.title;
+ document.getElementById('hc-lab-sub').textContent = lab.sub;
+ document.getElementById('hc-lab-subj').textContent = lab.subj;
+ document.getElementById('hc-lab-time').textContent = lab.time;
+ document.getElementById('hc-lab-level').textContent = lab.level;
+ document.getElementById('hc-lab-meta').textContent = 'Освой: ' + lab.goal;
}
/* ══ ACTIVITY: data structure ══════════════════════════════════════ */
@@ -3999,9 +4048,11 @@
loadSubjProgressWidget(rows);
renderStreakCalendar(rows);
} catch {}
+ const heroRow = document.getElementById('hero-row');
+ if (heroRow) heroRow.style.display = '';
loadContinueWidget();
- loadWeakWidget();
- loadTheoryWidget();
+ loadLabOfDay();
+ loadPetHero();
loadFlashcardWidget();
}
diff --git a/frontend/js/lab-previews.js b/frontend/js/lab-previews.js
new file mode 100644
index 0000000..6c5ae56
--- /dev/null
+++ b/frontend/js/lab-previews.js
@@ -0,0 +1,93 @@
+'use strict';
+/*
+ * LabPreviews — реальные SVG-превью симуляций (зеркало P_* из labs/lab-glue.js)
+ * для карточки «Лаборатория дня» на дашборде. Тёмные тайлы 270×140.
+ * Источник истины — lab-glue.js; здесь лёгкая копия, чтобы не грузить весь
+ * движок лаборатории на дашборде.
+ */
+(function () {
+ function _grid(fg='rgba(255,255,255,0.06)') {
+ return `
+
+
+
+
+
+ `;
+ }
+
+ function _svg(body) {
+ return `
`;
+ }
+
+ const P_LENS = _svg(`${_grid('rgba(255,255,255,0.04)')}
+
+
+
+
+
+
+
+
+
`);
+
+ const P_CIRCUIT = _svg(`${_grid('rgba(255,255,255,0.04)')}
+
+
+
+
R₁
+
+
+
R₂
+
+
+
+
I = U/R`);
+
+ const P_PENDULUM = _svg(`${_grid('rgba(255,255,255,0.04)')}
+
+
+
+
+
+
+
T = 2π√(l/g)`);
+
+ const P_WAVES = _svg(`${_grid()}
+
+
+
+
+
v = \u03bbf \u00b7 y = A sin(\u03c9t \u2212 kx) \u00b7 \u0441\u0442\u043e\u044f\u0447\u0438\u0435 \u0432\u043e\u043b\u043d\u044b`);
+
+ const P_ISOPROCESS = _svg(`${_grid('rgba(255,255,255,0.04)')}
+
+
+
+
+
+
+
+
+
+
2
+
1
+
V
+
P`);
+
+ const P_3D = _svg(`${_grid('rgba(255,255,255,0.04)')}
+
+
+
+
+
V = a³`);
+
+ window.LabPreviews = {
+ opticsbench: P_LENS, circuit: P_CIRCUIT, pendulum: P_PENDULUM,
+ waves: P_WAVES, isoprocess: P_ISOPROCESS, stereo: P_3D,
+ };
+})();
diff --git a/frontend/js/pet-sprite.js b/frontend/js/pet-sprite.js
new file mode 100644
index 0000000..344a15d
--- /dev/null
+++ b/frontend/js/pet-sprite.js
@@ -0,0 +1,318 @@
+'use strict';
+/*
+ * PetSprite — единый источник модели питомца для /pet и /dashboard.
+ * Канонические PET_PALETTES + shadeColor + renderPetSVG. pet.html и
+ * dashboard.html используют window.PetSprite.render(...) — без дублей.
+ */
+(function () {
+ const PET_PALETTES = {
+ purple:'#9B5DE5', cyan:'#06D6E0', gold:'#F9C74F',
+ red:'#F94144', green:'#38D95A', blue:'#4A90D9',
+};
+
+ function shadeColor(hex, pct) {
+ const n = parseInt(hex.slice(1), 16);
+ const r = Math.min(255,Math.max(0,(n>>16)+pct));
+ const g = Math.min(255,Math.max(0,((n>>8)&0xff)+pct));
+ const b = Math.min(255,Math.max(0,(n&0xff)+pct));
+ return `rgb(${r},${g},${b})`;
+}
+
+ function renderPetSVG(level, mood, accessories = [], colorKey = 'purple', streak = 0) {
+ const col = PET_PALETTES[colorKey] || '#9B5DE5';
+ const dark = shadeColor(col, -45);
+ const light = shadeColor(col, 52);
+ const uid = `pg${level}${mood[0]}${colorKey[0]}`;
+
+ const bodyPath = 'M55,22 C70,22 86,37 87,56 C89,75 78,94 55,97 C32,94 21,75 23,56 C24,37 40,22 55,22 Z';
+ const eyeY = 52, eyeX1 = 40, eyeX2 = 70;
+
+ /* ── Eyebrows ── */
+ let eyebrows = '';
+ if (mood !== 'sleeping') {
+ if (mood === 'ecstatic') {
+ eyebrows = `
+
`;
+ } else if (mood === 'sad' || mood === 'hungry') {
+ eyebrows = `
+
`;
+ } else {
+ eyebrows = `
+
`;
+ }
+ }
+
+ /* ── Eyes ── */
+ let eyeGroups = '', cheeks = '', extras = '';
+ if (mood === 'sleeping') {
+ eyeGroups = `
+
`;
+ } else {
+ const ry1 = mood === 'ecstatic' ? 13 : mood === 'happy' ? 12 : 10.5;
+ const prx = mood === 'ecstatic' ? 7 : mood === 'happy' ? 6.5 : 5.5;
+ const pry = mood === 'ecstatic' ? 8 : mood === 'happy' ? 7.5 : 6.5;
+ const pOff = (mood === 'sad' || mood === 'hungry') ? 3 : 2;
+ eyeGroups = `
+
+
+
+
+
+
+
+
+
+ `;
+ }
+
+ /* ── Nose ── */
+ const nose = mood !== 'sleeping'
+ ? `
` : '';
+
+ /* ── Mouth + cheeks ── */
+ let mouth = '';
+ if (mood === 'sleeping') {
+ mouth = `
`;
+ } else if (mood === 'ecstatic') {
+ mouth = `
`;
+ cheeks = `
+
`;
+ } else if (mood === 'happy') {
+ mouth = `
`;
+ cheeks = `
+
`;
+ } else if (mood === 'sad' || mood === 'hungry') {
+ mouth = `
`;
+ if (mood === 'hungry')
+ extras = `
`;
+ } else {
+ mouth = `
`;
+ }
+
+ /* ── Animated tail (all levels) ── */
+ const tail = `
+
+
+ `;
+
+ /* ── Ears (level 2+) ── */
+ let ears = '';
+ if (level >= 2) {
+ ears = `
+
+
+
`;
+ }
+
+ /* ── Antennae (level 3+) ── */
+ let antennae = '';
+ if (level >= 3) {
+ antennae = `
+
+
+
+
+
`;
+ }
+
+ /* ── Wings with flutter (level 4+) ── */
+ let wings = '';
+ if (level >= 4) {
+ wings = `
+
+
+
+
+
+
+
+
+ `;
+ }
+
+ /* ── Paws with fingers ── */
+ const paws = `
+
+
+
+
+
+
+
+
`;
+
+ /* ── Accessories ── */
+ let accSvg = '';
+ if (accessories.includes('hat')) {
+ accSvg += `
+
+
`;
+ }
+ if (accessories.includes('glasses')) {
+ accSvg += `
+
+
+
+
`;
+ }
+ if (accessories.includes('crown') || level >= 5) {
+ accSvg += `
+
+
+
+
`;
+ }
+ if (accessories.includes('star') && level < 5) {
+ accSvg += `
`;
+ }
+
+ /* ── Aura ring (level 4+) ── */
+ let aura = '';
+ if (level >= 4) {
+ aura = `
+
+
+
+
+ `;
+ }
+
+ /* ── B3 Rainbow collar (streak ≥ 7) ── */
+ let rainbowCollar = '';
+ if (streak >= 7) {
+ rainbowCollar = `
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ }
+
+ /* ── Orbital particles (level 5+) ── */
+ let orbitals = '';
+ if (level >= 5) {
+ const oData = [
+ { start: 0, dur: 2.8, r: 46, size: 4, fill: col, op: .85 },
+ { start: 120, dur: 3.6, r: 43, size: 3, fill: light, op: .7 },
+ { start: 240, dur: 2.2, r: 48, size: 3.5, fill: 'white', op: .6 },
+ ];
+ // Extra orbitals for level 6+
+ if (level >= 6) {
+ oData.push(
+ { start: 60, dur: 1.9, r: 51, size: 2.5, fill: light, op: .65 },
+ { start: 180, dur: 4.1, r: 40, size: 2, fill: col, op: .55 },
+ { start: 300, dur: 3.0, r: 54, size: 3, fill: 'white', op: .5 },
+ );
+ }
+ // Even more for level 8
+ if (level >= 8) {
+ oData.push(
+ { start: 45, dur: 1.5, r: 56, size: 3.5, fill: '#FFD700', op: .9 },
+ { start: 225, dur: 2.4, r: 38, size: 2.5, fill: '#FFD700', op: .75 },
+ );
+ }
+ orbitals = oData.map(o => `
+
+
+ `).join('\n');
+ }
+
+ /* ── Second aura ring (level 6+) ── */
+ if (level >= 6) {
+ aura += `
+
+
+
+
+ `;
+ }
+
+ /* ── Crystal halo (level 7+) ── */
+ let halo = '';
+ if (level >= 7) {
+ halo = `
+
+
+
+
+ `;
+ }
+
+ /* ── Second wing pair (level 7+) ── */
+ if (level >= 7) {
+ wings += `
+
+
+
+
+
+
+ `;
+ }
+
+ /* ── Third aura + body shimmer (level 8) ── */
+ let shimmer = '';
+ if (level >= 8) {
+ aura += `
+
+
+
+
+ `;
+ shimmer = `
`;
+ }
+
+ return `
`;
+}
+
+ var MOOD_RU = {
+ ecstatic: 'в восторге', happy: 'в духе', neutral: 'бодр',
+ sad: 'скучает', hungry: 'голоден', sleeping: 'спит',
+ };
+
+ window.PetSprite = {
+ PALETTES: PET_PALETTES,
+ shade: shadeColor,
+ render: renderPetSVG,
+ moodLabel: function (m) { return MOOD_RU[m] || 'бодр'; },
+ };
+})();