Sprint 18 Extra
📱 СПРИНТ 18: “ИНТЕРАКТИВНЫЙ ВЕБ-ИНТЕРФЕЙС УПРАВЛЕНИЯ”
От статического сайта к живому цифровому опыту
🎯 МЕТОДОЛОГИЧЕСКАЯ КОНЦЕПЦИЯ СПРИНТА
Философия перехода:
1БЫЛО: Веб-страница показывает данные (пассивное потребление)
2СТАЛО: Веб-интерфейс = живой организм (активное взаимодействие)
Ключевая идея: Дети создают “цифрового собеседника” - интерфейс, который не просто показывает информацию, а ведет диалог с пользователем, предугадывает потребности и адаптируется под каждого человека.
Концептуальная эволюция:
- Статика → Динамика: От показа данных к интерактивному опыту
- Монолог → Диалог: Система не просто говорит, но и слушает
- Универсальность → Персонализация: Интерфейс адаптируется под пользователя
- Инструмент → Партнер: Система становится цифровым помощником
🧠 ПЕДАГОГИЧЕСКИЕ ЦЕЛИ СПРИНТА
Концептуальные цели:
- Понимание “интерактивности” как двустороннего общения
- Осознание “адаптивности” интерфейсов под пользователя
- Введение понятий “UX/UI дизайна” - психология взаимодействия
- Понимание “персонализации” как основы современных систем
Технические цели:
- Продвинутый JavaScript для интерактивности
- CSS анимации и переходы для живости интерфейса
- Drag & Drop, жесты, голосовое управление
- Адаптивный дизайн для разных устройств
- Локальное хранение пользовательских настроек
Метакогнитивные цели:
- “Эмпатическое мышление” - понимание потребностей пользователя
- “Дизайн-мышление” - итеративное улучшение опыта
- “Поведенческое мышление” - как интерфейс влияет на действия
📚 СТРУКТУРА СПРИНТА (4 занятия)
Занятие 1: “Психология взаимодействия” 🧠
Длительность: 90 минут
Фаза 1: Философия интерактивности (20 мин)
Метод: Экспериментальная психология
Эксперимент “Разные способы общения”:
-
Бумажная записка (статический веб-сайт)
- Учитель пишет на доске: “Температура: 22°C”
- Ученики могут только смотреть
-
Разговор через переводчика (API)
- Ученик → переводчик → учитель → переводчик → ученик
- “Какая температура?” → “22°C”
-
Живое общение (интерактивный интерфейс)
- Прямой диалог, жесты, мимика, адаптация
Ключевые открытия:
- “Интерактивность = способность системы учиться от пользователя”
- “Хороший интерфейс предугадывает желания”
- “Система должна адаптироваться под человека, а не наоборот”
Фаза 2: Анализ пользовательского опыта (25 мин)
Метод: User Journey Mapping
Практическое упражнение: “День из жизни пользователя”
Дети анализируют, как разные люди будут взаимодействовать с их системой:
1🧑🏫 УЧИТЕЛЬ (утром):
2 Приходит → Хочет быстро подготовить класс
3 Потребность: "Один клик = комфортные условия"
4 Интерфейс: Большая кнопка "Подготовить к уроку"
5
6👨🔧 ЗАВХОЗ (вечером):
7 Проверяет → Хочет убедиться, что всё выключено
8 Потребность: "Быстрый обзор статуса всех систем"
9 Интерфейс: Панель мониторинга с индикаторами
10
11🧑🎓 УЧЕНИК (на перемене):
12 Играет → Хочет изменить освещение для селфи
13 Потребность: "Интуитивное управление без инструкций"
14 Интерфейс: Слайдеры, жесты, визуальная обратная связь
15
16👩💼 ДИРЕКТОР (удаленно):
17 Контролирует → Хочет видеть общую картину
18 Потребность: "Аналитика и отчеты"
19 Интерфейс: Графики, тренды, уведомления
Педагогический инсайт: “Один интерфейс ≠ один размер для всех. Нужна адаптация под роль и контекст!”
Фаза 3: Принципы интерактивного дизайна (30 мин)
Концепция: “Законы взаимодействия человека и машины”
10 принципов интерактивности для детей:
- Принцип отзывчивости: “Система всегда отвечает на действие”
- Принцип предсказуемости: “Похожие действия дают похожие результаты”
- Принцип обратной связи: “Пользователь всегда знает, что происходит”
- Принцип прощения: “Можно отменить любое действие”
- Принцип доступности: “Каждый может пользоваться системой”
- Принцип эффективности: “Частые действия должны быть простыми”
- Принцип красоты: “Приятный вид = желание пользоваться”
- Принцип персонализации: “Система помнит предпочтения”
- Принцип контекста: “Интерфейс адаптируется под ситуацию”
- Принцип обучения: “Система становится умнее от использования”
Практическое применение: Дети переделывают свой веб-интерфейс под каждый принцип:
1// Принцип отзывчивости
2button.addEventListener('click', function() {
3 // Немедленная визуальная реакция
4 this.style.transform = 'scale(0.95)';
5 this.style.backgroundColor = '#45a049';
6
7 // Звуковая обратная связь
8 playClickSound();
9
10 // Тактильная обратная связь (если поддерживается)
11 if (navigator.vibrate) {
12 navigator.vibrate(50);
13 }
14
15 setTimeout(() => {
16 this.style.transform = 'scale(1)';
17 this.style.backgroundColor = '';
18 }, 150);
19});
20
21// Принцип предсказуемости
22function createConsistentButton(text, action) {
23 const button = document.createElement('button');
24 button.textContent = text;
25 button.className = 'standard-button'; // Единый стиль
26 button.onclick = action;
27 return button;
28}
29
30// Принцип обратной связи
31function showLoadingState(element, message) {
32 element.innerHTML = `
33 <div class="loading-spinner"></div>
34 <span>${message}</span>
35 `;
36 element.disabled = true;
37}
38
39function showSuccessState(element, message) {
40 element.innerHTML = `
41 <div class="success-icon">✅</div>
42 <span>${message}</span>
43 `;
44 element.style.backgroundColor = '#4CAF50';
45}
Фаза 4: Прототипирование на бумаге (15 мин)
Метод: Paper Prototyping
Дети рисуют wireframes своего интерактивного интерфейса:
- Где какие элементы расположены
- Как они реагируют на клики/касания
- Какие анимации и переходы будут
- Как интерфейс адаптируется под разных пользователей
Занятие 2: “Живой интерфейс” ✨
Длительность: 90 минут
Фаза 1: CSS анимации и микровзаимодействия (35 мин)
Концепция: “Движение = жизнь интерфейса”
Практическая реализация живого интерфейса:
1<!DOCTYPE html>
2<html lang="ru">
3<head>
4 <meta charset="UTF-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 <title>🤖 ALEX - Интерактивная панель управления</title>
7 <style>
8 * {
9 margin: 0;
10 padding: 0;
11 box-sizing: border-box;
12 }
13
14 body {
15 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
16 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
17 min-height: 100vh;
18 overflow-x: hidden;
19 }
20
21 /* Анимированный фон */
22 .animated-background {
23 position: fixed;
24 top: 0;
25 left: 0;
26 width: 100%;
27 height: 100%;
28 z-index: -1;
29 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
30 background-size: 400% 400%;
31 animation: gradientShift 15s ease infinite;
32 }
33
34 @keyframes gradientShift {
35 0% { background-position: 0% 50%; }
36 50% { background-position: 100% 50%; }
37 100% { background-position: 0% 50%; }
38 }
39
40 /* Плавающие частицы */
41 .particle {
42 position: absolute;
43 background: rgba(255, 255, 255, 0.1);
44 border-radius: 50%;
45 animation: float 20s infinite linear;
46 }
47
48 @keyframes float {
49 0% {
50 transform: translateY(100vh) rotate(0deg);
51 opacity: 0;
52 }
53 10% {
54 opacity: 1;
55 }
56 90% {
57 opacity: 1;
58 }
59 100% {
60 transform: translateY(-100px) rotate(360deg);
61 opacity: 0;
62 }
63 }
64
65 .container {
66 max-width: 1200px;
67 margin: 0 auto;
68 padding: 20px;
69 position: relative;
70 z-index: 1;
71 }
72
73 /* Адаптивная шапка */
74 .header {
75 text-align: center;
76 margin-bottom: 30px;
77 animation: slideInDown 0.8s ease-out;
78 }
79
80 @keyframes slideInDown {
81 from {
82 transform: translateY(-50px);
83 opacity: 0;
84 }
85 to {
86 transform: translateY(0);
87 opacity: 1;
88 }
89 }
90
91 .system-title {
92 font-size: clamp(2rem, 5vw, 4rem);
93 color: white;
94 margin-bottom: 10px;
95 text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
96 animation: glow 2s ease-in-out infinite alternate;
97 }
98
99 @keyframes glow {
100 from {
101 text-shadow: 2px 2px 4px rgba(0,0,0,0.3), 0 0 10px rgba(255,255,255,0.2);
102 }
103 to {
104 text-shadow: 2px 2px 4px rgba(0,0,0,0.3), 0 0 20px rgba(255,255,255,0.4);
105 }
106 }
107
108 .status-indicator {
109 display: inline-flex;
110 align-items: center;
111 gap: 10px;
112 background: rgba(255,255,255,0.1);
113 padding: 10px 20px;
114 border-radius: 25px;
115 backdrop-filter: blur(10px);
116 border: 1px solid rgba(255,255,255,0.2);
117 transition: all 0.3s ease;
118 }
119
120 .status-indicator:hover {
121 transform: translateY(-2px);
122 box-shadow: 0 5px 15px rgba(0,0,0,0.2);
123 }
124
125 .status-dot {
126 width: 12px;
127 height: 12px;
128 border-radius: 50%;
129 background: #4CAF50;
130 animation: pulse 2s infinite;
131 }
132
133 @keyframes pulse {
134 0% {
135 box-shadow: 0 0 0 0 rgba(76, 175, 80, 0.7);
136 }
137 70% {
138 box-shadow: 0 0 0 10px rgba(76, 175, 80, 0);
139 }
140 100% {
141 box-shadow: 0 0 0 0 rgba(76, 175, 80, 0);
142 }
143 }
144
145 /* Интерактивная сетка датчиков */
146 .sensors-grid {
147 display: grid;
148 grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
149 gap: 25px;
150 margin-bottom: 40px;
151 }
152
153 .sensor-card {
154 background: rgba(255,255,255,0.1);
155 backdrop-filter: blur(15px);
156 border-radius: 20px;
157 padding: 25px;
158 border: 1px solid rgba(255,255,255,0.2);
159 transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
160 cursor: pointer;
161 position: relative;
162 overflow: hidden;
163 animation: slideInUp 0.6s ease-out;
164 animation-fill-mode: backwards;
165 }
166
167 .sensor-card:nth-child(1) { animation-delay: 0.1s; }
168 .sensor-card:nth-child(2) { animation-delay: 0.2s; }
169 .sensor-card:nth-child(3) { animation-delay: 0.3s; }
170 .sensor-card:nth-child(4) { animation-delay: 0.4s; }
171
172 @keyframes slideInUp {
173 from {
174 transform: translateY(50px);
175 opacity: 0;
176 }
177 to {
178 transform: translateY(0);
179 opacity: 1;
180 }
181 }
182
183 .sensor-card:hover {
184 transform: translateY(-10px) scale(1.02);
185 box-shadow: 0 20px 40px rgba(0,0,0,0.2);
186 border-color: rgba(255,255,255,0.4);
187 }
188
189 .sensor-card:active {
190 transform: translateY(-5px) scale(0.98);
191 }
192
193 /* Анимированная волна для активной карточки */
194 .sensor-card::before {
195 content: '';
196 position: absolute;
197 top: 0;
198 left: -100%;
199 width: 100%;
200 height: 100%;
201 background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
202 transition: left 0.5s;
203 }
204
205 .sensor-card:hover::before {
206 left: 100%;
207 }
208
209 .sensor-icon {
210 font-size: 3.5rem;
211 margin-bottom: 15px;
212 display: block;
213 transition: transform 0.3s ease;
214 }
215
216 .sensor-card:hover .sensor-icon {
217 transform: scale(1.2) rotate(5deg);
218 }
219
220 .sensor-name {
221 font-size: 1.1rem;
222 color: rgba(255,255,255,0.8);
223 margin-bottom: 10px;
224 font-weight: 500;
225 }
226
227 .sensor-value {
228 font-size: 2.5rem;
229 font-weight: bold;
230 color: white;
231 margin: 15px 0;
232 transition: all 0.3s ease;
233 }
234
235 .sensor-status {
236 font-size: 0.9rem;
237 color: rgba(255,255,255,0.7);
238 font-style: italic;
239 }
240
241 /* Интерактивные элементы управления */
242 .controls-section {
243 margin-bottom: 40px;
244 }
245
246 .section-title {
247 color: white;
248 font-size: 1.5rem;
249 margin-bottom: 20px;
250 text-align: center;
251 opacity: 0;
252 animation: fadeIn 0.8s ease-out 0.5s forwards;
253 }
254
255 @keyframes fadeIn {
256 to { opacity: 1; }
257 }
258
259 .controls-grid {
260 display: grid;
261 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
262 gap: 20px;
263 }
264
265 /* Умные кнопки */
266 .smart-button {
267 background: rgba(255,255,255,0.15);
268 border: 2px solid rgba(255,255,255,0.3);
269 border-radius: 15px;
270 padding: 20px;
271 color: white;
272 font-size: 1rem;
273 font-weight: 500;
274 cursor: pointer;
275 transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
276 position: relative;
277 overflow: hidden;
278 text-align: center;
279 user-select: none;
280 }
281
282 .smart-button:hover {
283 transform: translateY(-3px);
284 box-shadow: 0 10px 25px rgba(0,0,0,0.2);
285 border-color: rgba(255,255,255,0.5);
286 background: rgba(255,255,255,0.2);
287 }
288
289 .smart-button:active {
290 transform: translateY(-1px);
291 transition: transform 0.1s;
292 }
293
294 /* Кнопка с пульсацией для важных действий */
295 .smart-button.primary {
296 background: linear-gradient(45deg, #FF6B6B, #4ECDC4);
297 border-color: transparent;
298 animation: primaryPulse 3s infinite;
299 }
300
301 @keyframes primaryPulse {
302 0%, 100% {
303 box-shadow: 0 5px 15px rgba(255, 107, 107, 0.4);
304 }
305 50% {
306 box-shadow: 0 5px 25px rgba(255, 107, 107, 0.6);
307 }
308 }
309
310 /* Рипл-эффект для кнопок */
311 .smart-button::after {
312 content: '';
313 position: absolute;
314 top: 50%;
315 left: 50%;
316 width: 0;
317 height: 0;
318 border-radius: 50%;
319 background: rgba(255,255,255,0.3);
320 transform: translate(-50%, -50%);
321 transition: width 0.6s, height 0.6s;
322 }
323
324 .smart-button:active::after {
325 width: 300px;
326 height: 300px;
327 }
328
329 /* Продвинутая панель статуса */
330 .status-panel {
331 background: rgba(0,0,0,0.2);
332 border-radius: 15px;
333 padding: 25px;
334 margin-top: 30px;
335 backdrop-filter: blur(10px);
336 border: 1px solid rgba(255,255,255,0.1);
337 }
338
339 .system-stats {
340 display: grid;
341 grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
342 gap: 20px;
343 margin-bottom: 20px;
344 }
345
346 .stat-item {
347 text-align: center;
348 color: white;
349 }
350
351 .stat-value {
352 font-size: 2rem;
353 font-weight: bold;
354 margin-bottom: 5px;
355 color: #4ECDC4;
356 }
357
358 .stat-label {
359 font-size: 0.9rem;
360 opacity: 0.8;
361 }
362
363 /* Анимированный прогресс-бар */
364 .progress-bar {
365 width: 100%;
366 height: 8px;
367 background: rgba(255,255,255,0.2);
368 border-radius: 4px;
369 overflow: hidden;
370 margin: 10px 0;
371 }
372
373 .progress-fill {
374 height: 100%;
375 background: linear-gradient(90deg, #4ECDC4, #44A08D);
376 border-radius: 4px;
377 transition: width 0.5s ease;
378 position: relative;
379 }
380
381 .progress-fill::after {
382 content: '';
383 position: absolute;
384 top: 0;
385 left: 0;
386 right: 0;
387 bottom: 0;
388 background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
389 animation: progressShine 2s infinite;
390 }
391
392 @keyframes progressShine {
393 0% { transform: translateX(-100%); }
394 100% { transform: translateX(100%); }
395 }
396
397 /* Адаптивность для мобильных устройств */
398 @media (max-width: 768px) {
399 .container {
400 padding: 15px;
401 }
402
403 .sensor-card {
404 padding: 20px;
405 }
406
407 .sensor-value {
408 font-size: 2rem;
409 }
410
411 .smart-button {
412 padding: 15px;
413 font-size: 0.9rem;
414 }
415 }
416
417 /* Состояния для разных типов данных */
418 .sensor-card.temperature {
419 border-left: 4px solid #FF6B6B;
420 }
421
422 .sensor-card.light {
423 border-left: 4px solid #FFD93D;
424 }
425
426 .sensor-card.sound {
427 border-left: 4px solid #6BCF7F;
428 }
429
430 .sensor-card.motion {
431 border-left: 4px solid #4D96FF;
432 }
433
434 /* Тематические изменения в зависимости от времени */
435 body.night-mode {
436 --primary-color: #2c3e50;
437 --accent-color: #3498db;
438 }
439
440 body.day-mode {
441 --primary-color: #f39c12;
442 --accent-color: #e74c3c;
443 }
444 </style>
445</head>
446<body>
447 <div class="animated-background"></div>
448
449 <!-- Генерируем плавающие частицы -->
450 <script>
451 // Создаем плавающие частицы
452 for(let i = 0; i < 20; i++) {
453 setTimeout(() => {
454 createParticle();
455 }, i * 300);
456 }
457
458 function createParticle() {
459 const particle = document.createElement('div');
460 particle.className = 'particle';
461 particle.style.left = Math.random() * 100 + '%';
462 particle.style.width = particle.style.height = (Math.random() * 10 + 5) + 'px';
463 particle.style.animationDuration = (Math.random() * 15 + 10) + 's';
464 particle.style.animationDelay = Math.random() * 2 + 's';
465
466 document.body.appendChild(particle);
467
468 // Удаляем частицу после анимации
469 setTimeout(() => {
470 if (particle.parentNode) {
471 particle.parentNode.removeChild(particle);
472 }
473 createParticle(); // Создаем новую
474 }, 20000);
475 }
476 </script>
477
478 <div class="container">
479 <!-- Умная шапка -->
480 <header class="header">
481 <h1 class="system-title">🤖 ALEX</h1>
482 <div class="status-indicator">
483 <div class="status-dot"></div>
484 <span id="systemStatus">Активна и заботится о классе</span>
485 </div>
486 </header>
487
488 <!-- Интерактивные датчики -->
489 <section class="sensors-grid">
490 <div class="sensor-card temperature" data-sensor="temperature">
491 <span class="sensor-icon">🌡️</span>
492 <div class="sensor-name">Температура</div>
493 <div class="sensor-value" id="temperature">--°C</div>
494 <div class="sensor-status" id="tempStatus">Загрузка...</div>
495 <div class="progress-bar">
496 <div class="progress-fill" id="tempProgress" style="width: 0%"></div>
497 </div>
498 </div>
499
500 <div class="sensor-card light" data-sensor="light">
501 <span class="sensor-icon">💡</span>
502 <div class="sensor-name">Освещенность</div>
503 <div class="sensor-value" id="light">--%</div>
504 <div class="sensor-status" id="lightStatus">Загрузка...</div>
505 <div class="progress-bar">
506 <div class="progress-fill" id="lightProgress" style="width: 0%"></div>
507 </div>
508 </div>
509
510 <div class="sensor-card sound" data-sensor="sound">
511 <span class="sensor-icon">🔊</span>
512 <div class="sensor-name">Уровень шума</div>
513 <div class="sensor-value" id="sound">--дБ</div>
514 <div class="sensor-status" id="soundStatus">Загрузка...</div>
515 <div class="progress-bar">
516 <div class="progress-fill" id="soundProgress" style="width: 0%"></div>
517 </div>
518 </div>
519
520 <div class="sensor-card motion" data-sensor="motion">
521 <span class="sensor-icon">🚶</span>
522 <div class="sensor-name">Движение</div>
523 <div class="sensor-value" id="motion">--</div>
524 <div class="sensor-status" id="motionStatus">Загрузка...</div>
525 <div class="progress-bar">
526 <div class="progress-fill" id="motionProgress" style="width: 0%"></div>
527 </div>
528 </div>
529 </section>
530
531 <!-- Интерактивное управление -->
532 <section class="controls-section">
533 <h2 class="section-title">🎛️ Умное управление</h2>
534 <div class="controls-grid">
535 <button class="smart-button primary" onclick="smartControl('comfort')">
536 😌 Комфортный режим
537 </button>
538 <button class="smart-button" onclick="smartControl('eco')">
539 🌱 Эко-режим
540 </button>
541 <button class="smart-button" onclick="smartControl('party')">
542 🎉 Вечеринка
543 </button>
544 <button class="smart-button" onclick="smartControl('focus')">
545 🎯 Фокус на учебе
546 </button>
547 <button class="smart-button" onclick="smartControl('presentation')">
548 📽️ Презентация
549 </button>
550 <button class="smart-button" onclick="smartControl('break')">
551 ☕ Перемена
552 </button>
553 </div>
554 </section>
555
556 <!-- Статус панель -->
557 <section class="status-panel">
558 <div class="system-stats">
559 <div class="stat-item">
560 <div class="stat-value" id="uptime">0</div>
561 <div class="stat-label">Время работы (мин)</div>
562 </div>
563 <div class="stat-item">
564 <div class="stat-value" id="decisions">0</div>
565 <div class="stat-label">Принято решений</div>
566 </div>
567 <div class="stat-item">
568 <div class="stat-value" id="energy">100</div>
569 <div class="stat-label">Энергия (%)</div>
570 </div>
571 <div class="stat-item">
572 <div class="stat-value" id="happiness">😊</div>
573 <div class="stat-label">Настроение класса</div>
574 </div>
575 </div>
576
577 <div style="text-align: center; color: white; margin-top: 15px;">
578 <strong>🧠 Последнее решение:</strong>
579 <span id="lastDecision">Система запускается...</span>
580 </div>
581 </section>
582 </div>
583
584 <script>
585 // Глобальные переменные
586 let sensorData = {};
587 let systemStats = {
588 startTime: Date.now(),
589 decisions: 0,
590 energy: 100,
591 mood: '😊'
592 };
593
594 // Установка WebSocket соединения
595 const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
596 const socket = new WebSocket(`${wsProtocol}//${window.location.hostname}:81`);
597
598 socket.onopen = function() {
599 console.log('🔗 Подключен к системе в реальном времени');
600 updateSystemStatus('🟢 Подключен к системе', 'success');
601 };
602
603 socket.onmessage = function(event) {
604 try {
605 const data = JSON.parse(event.data);
606 if (data.type === 'sensor_update') {
607 updateSensorData(data);
608 }
609 } catch (e) {
610 console.log('💬 Сообщение от системы:', event.data);
611 }
612 };
613
614 socket.onerror = function() {
615 updateSystemStatus('🔴 Ошибка соединения', 'error');
616 };
617
618 // Обновление данных датчиков с анимацией
619 function updateSensorData(data) {
620 sensorData = data;
621
622 // Анимированное обновление температуры
623 animateValueUpdate('temperature', data.temperature + '°C');
624 updateProgressBar('tempProgress', data.temperature, 0, 40);
625 updateSensorStatus('tempStatus', getTemperatureStatus(data.temperature));
626
627 // Анимированное обновление освещенности
628 const lightPercent = Math.round((data.light / 4095) * 100);
629 animateValueUpdate('light', lightPercent + '%');
630 updateProgressBar('lightProgress', lightPercent, 0, 100);
631 updateSensorStatus('lightStatus', getLightStatus(lightPercent));
632
633 // Анимированное обновление звука
634 const soundPercent = Math.round((data.sound / 4095) * 100);
635 animateValueUpdate('sound', soundPercent + 'дБ');
636 updateProgressBar('soundProgress', soundPercent, 0, 100);
637 updateSensorStatus('soundStatus', getSoundStatus(soundPercent));
638
639 // Анимированное обновление движения
640 animateValueUpdate('motion', data.motion ? 'Есть' : 'Нет');
641 updateProgressBar('motionProgress', data.motion ? 100 : 0, 0, 100);
642 updateSensorStatus('motionStatus', getMotionStatus(data.motion));
643
644 // Обновляем настроение класса
645 updateClassMood(data);
646 }
647
648 function animateValueUpdate(elementId, newValue) {
649 const element = document.getElementById(elementId);
650 const oldValue = element.textContent;
651
652 if (oldValue !== newValue) {
653 // Анимация изменения
654 element.style.transform = 'scale(1.2)';
655 element.style.color = '#FFD93D';
656
657 setTimeout(() => {
658 element.textContent = newValue;
659 element.style.transform = 'scale(1)';
660 element.style.color = '';
661 }, 150);
662
663 // Добавляем пульсацию к родительской карточке
664 const card = element.closest('.sensor-card');
665 card.style.boxShadow = '0 0 30px rgba(255, 217, 61, 0.5)';
666 setTimeout(() => {
667 card.style.boxShadow = '';
668 }, 500);
669 }
670 }
671
672 function updateProgressBar(progressId, value, min, max) {
673 const progressElement = document.getElementById(progressId);
674 const percentage = ((value - min) / (max - min)) * 100;
675 progressElement.style.width = Math.max(0, Math.min(100, percentage)) + '%';
676 }
677
678 function updateSensorStatus(statusId, status) {
679 const statusElement = document.getElementById(statusId);
680 statusElement.textContent = status.message;
681 statusElement.style.color = status.color;
682 }
683
684 // Умные статусы для датчиков
685 function getTemperatureStatus(temp) {
686 if (temp < 18) return { message: '🥶 Холодно, включаю обогрев', color: '#4FC3F7' };
687 if (temp > 26) return { message: '🔥 Жарко, нужно охлаждение', color: '#FF7043' };
688 if (temp >= 20 && temp <= 24) return { message: '😌 Идеальная температура', color: '#66BB6A' };
689 return { message: '🌡️ Нормальная температура', color: '#FFD54F' };
690 }
691
692 function getLightStatus(light) {
693 if (light < 20) return { message: '🌙 Темно, включаю свет', color: '#9575CD' };
694 if (light > 80) return { message: '☀️ Очень светло', color: '#FFB74D' };
695 return { message: '👍 Хорошее освещение', color: '#81C784' };
696 }
697
698 function getSoundStatus(sound) {
699 if (sound < 30) return { message: '🤫 Очень тихо', color: '#4DB6AC' };
700 if (sound > 70) return { message: '📢 Шумно! Попрошу потише', color: '#E57373' };
701 return { message: '🎵 Комфортный уровень шума', color: '#AED581' };
702 }
703
704 function getMotionStatus(motion) {
705 return motion ?
706 { message: '👥 Люди в классе', color: '#64B5F6' } :
707 { message: '🏫 Класс пустой', color: '#90A4AE' };
708 }
709
710 // Определение настроения класса на основе всех данных
711 function updateClassMood(data) {
712 const temp = data.temperature;
713 const light = (data.light / 4095) * 100;
714 const sound = (data.sound / 4095) * 100;
715 const motion = data.motion;
716
717 let mood = '😐';
718 let moodText = 'Нейтральное';
719
720 // Алгоритм определения настроения
721 if (motion && temp >= 20 && temp <= 24 && light >= 40 && light <= 80 && sound < 60) {
722 mood = '😊'; moodText = 'Отличное';
723 } else if (!motion) {
724 mood = '😴'; moodText = 'Спокойное';
725 } else if (temp < 18 || temp > 26) {
726 mood = '😰'; moodText = 'Дискомфорт';
727 } else if (sound > 80) {
728 mood = '😵'; moodText = 'Хаос';
729 } else if (light < 20) {
730 mood = '😔'; moodText = 'Мрачное';
731 } else {
732 mood = '🙂'; moodText = 'Нормальное';
733 }
734
735 document.getElementById('happiness').textContent = mood;
736 systemStats.mood = moodText;
737 }
738
739 // Умное управление с контекстом
740 async function smartControl(mode) {
741 const button = event.target;
742
743 // Визуальная обратная связь
744 button.style.transform = 'scale(0.95)';
745 button.innerHTML = '⏳ Применяю...';
746 button.disabled = true;
747
748 try {
749 const response = await fetch(`/api/smart-mode/${mode}`, {
750 method: 'POST',
751 headers: { 'Content-Type': 'application/json' },
752 body: JSON.stringify({
753 currentSensors: sensorData,
754 userContext: getUserContext(),
755 timestamp: Date.now()
756 })
757 });
758
759 const result = await response.json();
760
761 if (result.success) {
762 button.innerHTML = '✅ Готово!';
763 button.style.backgroundColor = '#4CAF50';
764 updateLastDecision(result.decision);
765 systemStats.decisions++;
766
767 // Показываем детали изменений
768 showSmartModeDetails(mode, result.changes);
769
770 } else {
771 button.innerHTML = '❌ Ошибка';
772 button.style.backgroundColor = '#f44336';
773 }
774
775 } catch (error) {
776 button.innerHTML = '❌ Сбой';
777 button.style.backgroundColor = '#f44336';
778 console.error('Ошибка управления:', error);
779 }
780
781 // Возвращаем кнопку в исходное состояние
782 setTimeout(() => {
783 button.innerHTML = getButtonText(mode);
784 button.style.backgroundColor = '';
785 button.style.transform = '';
786 button.disabled = false;
787 }, 2000);
788 }
789
790 function getUserContext() {
791 const hour = new Date().getHours();
792 const isWeekend = [0, 6].includes(new Date().getDay());
793
794 return {
795 timeOfDay: hour < 12 ? 'morning' : hour < 17 ? 'afternoon' : 'evening',
796 isWeekend: isWeekend,
797 deviceType: /Mobi|Android/i.test(navigator.userAgent) ? 'mobile' : 'desktop',
798 batteryLevel: navigator.getBattery ? 'unknown' : 'desktop' // Упрощенно
799 };
800 }
801
802 function showSmartModeDetails(mode, changes) {
803 // Создаем всплывающее уведомление с деталями
804 const notification = document.createElement('div');
805 notification.className = 'smart-notification';
806 notification.innerHTML = `
807 <div class="notification-content">
808 <h3>🎯 Режим "${getModeTitle(mode)}" активирован</h3>
809 <ul>
810 ${changes.map(change => `<li>${change}</li>`).join('')}
811 </ul>
812 </div>
813 `;
814
815 // Стилизация уведомления
816 notification.style.cssText = `
817 position: fixed;
818 top: 20px;
819 right: 20px;
820 background: rgba(255,255,255,0.95);
821 color: #333;
822 padding: 20px;
823 border-radius: 15px;
824 box-shadow: 0 10px 30px rgba(0,0,0,0.3);
825 backdrop-filter: blur(10px);
826 border: 1px solid rgba(255,255,255,0.3);
827 max-width: 300px;
828 z-index: 1000;
829 animation: slideInRight 0.5s ease-out;
830 `;
831
832 document.body.appendChild(notification);
833
834 // Автоматическое удаление через 5 секунд
835 setTimeout(() => {
836 notification.style.animation = 'slideOutRight 0.5s ease-in';
837 setTimeout(() => notification.remove(), 500);
838 }, 5000);
839 }
840
841 function getModeTitle(mode) {
842 const titles = {
843 'comfort': 'Комфорт',
844 'eco': 'Эко-режим',
845 'party': 'Вечеринка',
846 'focus': 'Фокус',
847 'presentation': 'Презентация',
848 'break': 'Перемена'
849 };
850 return titles[mode] || mode;
851 }
852
853 function getButtonText(mode) {
854 const texts = {
855 'comfort': '😌 Комфортный режим',
856 'eco': '🌱 Эко-режим',
857 'party': '🎉 Вечеринка',
858 'focus': '🎯 Фокус на учебе',
859 'presentation': '📽️ Презентация',
860 'break': '☕ Перемена'
861 };
862 return texts[mode] || mode;
863 }
864
865 function updateLastDecision(decision) {
866 document.getElementById('lastDecision').textContent = decision;
867 }
868
869 function updateSystemStatus(status, type) {
870 document.getElementById('systemStatus').textContent = status;
871 const statusDot = document.querySelector('.status-dot');
872
873 if (type === 'success') {
874 statusDot.style.backgroundColor = '#4CAF50';
875 } else if (type === 'error') {
876 statusDot.style.backgroundColor = '#f44336';
877 } else {
878 statusDot.style.backgroundColor = '#FF9800';
879 }
880 }
881
882 // Обновление статистики системы
883 function updateSystemStats() {
884 const uptime = Math.floor((Date.now() - systemStats.startTime) / 60000);
885 document.getElementById('uptime').textContent = uptime;
886 document.getElementById('decisions').textContent = systemStats.decisions;
887 document.getElementById('energy').textContent = Math.max(0, systemStats.energy);
888
889 // Постепенное снижение энергии
890 if (systemStats.energy > 0) {
891 systemStats.energy -= 0.1;
892 }
893 }
894
895 // Адаптация интерфейса под время суток
896 function adaptToTimeOfDay() {
897 const hour = new Date().getHours();
898
899 if (hour >= 6 && hour < 18) {
900 document.body.classList.add('day-mode');
901 document.body.classList.remove('night-mode');
902 } else {
903 document.body.classList.add('night-mode');
904 document.body.classList.remove('day-mode');
905 }
906 }
907
908 // Инициализация
909 document.addEventListener('DOMContentLoaded', function() {
910 adaptToTimeOfDay();
911 updateSystemStats();
912
913 // Обновляем статистику каждую минуту
914 setInterval(updateSystemStats, 60000);
915
916 // Адаптируем интерфейс каждый час
917 setInterval(adaptToTimeOfDay, 3600000);
918
919 // Добавляем CSS анимации динамически
920 const style = document.createElement('style');
921 style.textContent = `
922 @keyframes slideInRight {
923 from { transform: translateX(100%); opacity: 0; }
924 to { transform: translateX(0); opacity: 1; }
925 }
926
927 @keyframes slideOutRight {
928 from { transform: translateX(0); opacity: 1; }
929 to { transform: translateX(100%); opacity: 0; }
930 }
931
932 .smart-notification h3 {
933 margin: 0 0 10px 0;
934 color: #2c3e50;
935 }
936
937 .smart-notification ul {
938 margin: 0;
939 padding-left: 20px;
940 }
941
942 .smart-notification li {
943 margin: 5px 0;
944 color: #555;
945 }
946 `;
947 document.head.appendChild(style);
948 });
949
950 // Добавляем обработчики для сенсорных карточек
951 document.querySelectorAll('.sensor-card').forEach(card => {
952 card.addEventListener('click', function() {
953 const sensorType = this.dataset.sensor;
954 showSensorDetails(sensorType);
955 });
956 });
957
958 function showSensorDetails(sensorType) {
959 // Здесь можно показать детальную информацию о датчике
960 console.log(`Показать детали для датчика: ${sensorType}`);
961 // Например, график изменений за последний час
962 }
963 </script>
964</body>
965</html>
Фаза 2: Адаптивное поведение (30 мин)
Концепция: “Интерфейс, который учится и адаптируется”
Серверная часть с адаптивной логикой:
1// Дополнения к ESP32 коду для умных режимов
2struct UserPreferences {
3 String preferredMode;
4 int comfortTemperature;
5 int preferredLightLevel;
6 bool soundSensitive;
7 unsigned long lastActiveTime;
8 int usageCount[6]; // счетчики использования режимов
9};
10
11UserPreferences userPrefs = {
12 .preferredMode = "comfort",
13 .comfortTemperature = 22,
14 .preferredLightLevel = 60,
15 .soundSensitive = false,
16 .lastActiveTime = 0,
17 .usageCount = {0, 0, 0, 0, 0, 0}
18};
19
20void handleSmartModeAPI() {
21 if (!server.hasArg("plain")) {
22 server.send(400, "application/json", "{\"error\": \"Нет данных\"}");
23 return;
24 }
25
26 String requestBody = server.arg("plain");
27 DynamicJsonDocument requestDoc(1024);
28 deserializeJson(requestDoc, requestBody);
29
30 String mode = server.uri().substring(server.uri().lastIndexOf('/') + 1);
31
32 // Анализируем контекст пользователя
33 JsonObject currentSensors = requestDoc["currentSensors"];
34 JsonObject userContext = requestDoc["userContext"];
35
36 // Адаптивная логика для каждого режима
37 SmartModeResult result = executeSmartMode(mode, currentSensors, userContext);
38
39 // Запоминаем предпочтения пользователя
40 learnFromUserChoice(mode, currentSensors);
41
42 // Отправляем результат
43 DynamicJsonDocument responseDoc(1024);
44 responseDoc["success"] = true;
45 responseDoc["mode"] = mode;
46 responseDoc["decision"] = result.decision;
47 responseDoc["changes"] = result.changes;
48 responseDoc["energy_saved"] = result.energySaved;
49 responseDoc["comfort_level"] = result.comfortLevel;
50
51 String response;
52 serializeJson(responseDoc, response);
53
54 server.send(200, "application/json", response);
55
56 logEvent("INFO", "Применен умный режим: " + mode);
57}
58
59struct SmartModeResult {
60 String decision;
61 JsonArray changes;
62 int energySaved;
63 int comfortLevel;
64};
65
66SmartModeResult executeSmartMode(String mode, JsonObject sensors, JsonObject context) {
67 SmartModeResult result;
68 DynamicJsonDocument changesDoc(512);
69 result.changes = changesDoc.to<JsonArray>();
70
71 float temperature = sensors["temperature"];
72 int light = sensors["light"];
73 int sound = sensors["sound"];
74 bool motion = sensors["motion"];
75
76 String timeOfDay = context["timeOfDay"];
77 bool isWeekend = context["isWeekend"];
78 String deviceType = context["deviceType"];
79
80 if (mode == "comfort") {
81 result = applyComfortMode(temperature, light, sound, motion, timeOfDay);
82 } else if (mode == "eco") {
83 result = applyEcoMode(temperature, light, sound, motion, timeOfDay);
84 } else if (mode == "party") {
85 result = applyPartyMode(temperature, light, sound, motion);
86 } else if (mode == "focus") {
87 result = applyFocusMode(temperature, light, sound, motion, timeOfDay);
88 } else if (mode == "presentation") {
89 result = applyPresentationMode(temperature, light, sound, motion);
90 } else if (mode == "break") {
91 result = applyBreakMode(temperature, light, sound, motion, timeOfDay);
92 }
93
94 return result;
95}
96
97SmartModeResult applyComfortMode(float temp, int light, int sound, bool motion, String timeOfDay) {
98 SmartModeResult result;
99 DynamicJsonDocument changesDoc(512);
100 result.changes = changesDoc.to<JsonArray>();
101
102 result.decision = "Настраиваю идеальные условия для комфорта";
103 result.comfortLevel = 90;
104 result.energySaved = 0;
105
106 // Умная настройка температуры с учетом времени дня
107 int targetTemp = userPrefs.comfortTemperature;
108 if (timeOfDay == "morning") targetTemp -= 1; // Утром чуть прохладнее
109 if (timeOfDay == "evening") targetTemp += 1; // Вечером чуть теплее
110
111 if (temp < targetTemp - 1) {
112 currentState.heaterOn = true;
113 digitalWrite(heaterPin, HIGH);
114 result.changes.add("🔥 Включил обогрев до " + String(targetTemp) + "°C");
115 } else if (temp > targetTemp + 1) {
116 currentState.heaterOn = false;
117 digitalWrite(heaterPin, LOW);
118 result.changes.add("❄️ Выключил обогрев - достаточно тепло");
119 }
120
121 // Умное освещение
122 int targetLight = userPrefs.preferredLightLevel;
123 if (timeOfDay == "morning") targetLight += 20; // Утром ярче
124 if (timeOfDay == "evening") targetLight -= 10; // Вечером мягче
125
126 int currentLightPercent = (light * 100) / 4095;
127 if (currentLightPercent < targetLight) {
128 currentState.lampOn = true;
129 digitalWrite(lampPin, HIGH);
130 result.changes.add("💡 Включил освещение для комфорта");
131 }
132
133 // Контроль шума
134 if (userPrefs.soundSensitive && sound > 2000) {
135 result.changes.add("🔇 Рекомендую снизить уровень шума");
136 }
137
138 return result;
139}
140
141SmartModeResult applyEcoMode(float temp, int light, int sound, bool motion, String timeOfDay) {
142 SmartModeResult result;
143 DynamicJsonDocument changesDoc(512);
144 result.changes = changesDoc.to<JsonArray>();
145
146 result.decision = "Оптимизирую энергопотребление с умом";
147 result.comfortLevel = 70;
148 result.energySaved = 30;
149
150 if (!motion) {
151 // Если нет людей - экономим по максимуму
152 currentState.heaterOn = false;
153 currentState.lampOn = false;
154 digitalWrite(heaterPin, LOW);
155 digitalWrite(lampPin, LOW);
156 result.changes.add("💚 Выключил все - класс пустой");
157 result.energySaved = 50;
158 } else {
159 // Есть люди - экономим разумно
160 if (temp > 20) { // Только если не холодно
161 currentState.heaterOn = false;
162 digitalWrite(heaterPin, LOW);
163 result.changes.add("🌡️ Выключил обогрев - температура приемлемая");
164 }
165
166 int currentLightPercent = (light * 100) / 4095;
167 if (currentLightPercent > 40) { // Если есть естественное освещение
168 currentState.lampOn = false;
169 digitalWrite(lampPin, LOW);
170 result.changes.add("☀️ Выключил искусственное освещение");
171 }
172 }
173
174 return result;
175}
176
177SmartModeResult applyPartyMode(float temp, int light, int sound, bool motion) {
178 SmartModeResult result;
179 DynamicJsonDocument changesDoc(512);
180 result.changes = changesDoc.to<JsonArray>();
181
182 result.decision = "Создаю атмосферу для веселья! 🎉";
183 result.comfortLevel = 85;
184 result.energySaved = -20; // Тратим больше энергии
185
186 // Яркое освещение
187 currentState.lampOn = true;
188 digitalWrite(lampPin, HIGH);
189 result.changes.add("🌟 Включил яркое освещение");
190
191 // Комфортная температура для активности
192 if (temp < 21) {
193 currentState.heaterOn = true;
194 digitalWrite(heaterPin, HIGH);
195 result.changes.add("🔥 Подогрел для активных игр");
196 }
197
198 result.changes.add("🎵 Разрешен повышенный уровень шума");
199 result.changes.add("💃 Время веселиться!");
200
201 return result;
202}
203
204SmartModeResult applyFocusMode(float temp, int light, int sound, bool motion, String timeOfDay) {
205 SmartModeResult result;
206 DynamicJsonDocument changesDoc(512);
207 result.changes = changesDoc.to<JsonArray>();
208
209 result.decision = "Создаю идеальные условия для концентрации";
210 result.comfortLevel = 95;
211 result.energySaved = 10;
212
213 // Оптимальная температура для мозговой активности
214 int focusTemp = 21; // Научно обоснованная температура
215 if (temp < focusTemp - 0.5) {
216 currentState.heaterOn = true;
217 digitalWrite(heaterPin, HIGH);
218 result.changes.add("🧠 Настроил температуру для концентрации (21°C)");
219 } else if (temp > focusTemp + 0.5) {
220 currentState.heaterOn = false;
221 digitalWrite(heaterPin, LOW);
222 result.changes.add("❄️ Снизил температуру - жара мешает думать");
223 }
224
225 // Оптимальное освещение
226 int currentLightPercent = (light * 100) / 4095;
227 if (currentLightPercent < 70) {
228 currentState.lampOn = true;
229 digitalWrite(lampPin, HIGH);
230 result.changes.add("💡 Увеличил освещение для чтения");
231 }
232
233 // Контроль шума
234 if (sound > 1500) {
235 result.changes.add("🤫 Попрошу соблюдать тишину для концентрации");
236 }
237
238 result.changes.add("📚 Режим глубокой концентрации активирован");
239
240 return result;
241}
242
243SmartModeResult applyPresentationMode(float temp, int light, int sound, bool motion) {
244 SmartModeResult result;
245 DynamicJsonDocument changesDoc(512);
246 result.changes = changesDoc.to<JsonArray>();
247
248 result.decision = "Подготавливаю класс для презентации";
249 result.comfortLevel = 88;
250 result.energySaved = 5;
251
252 // Комфортная температура для аудитории
253 if (temp < 22) {
254 currentState.heaterOn = true;
255 digitalWrite(heaterPin, HIGH);
256 result.changes.add("🌡️ Подогрел для комфорта аудитории");
257 }
258
259 // Среднее освещение (не слишком ярко для проектора)
260 int currentLightPercent = (light * 100) / 4095;
261 if (currentLightPercent > 60) {
262 currentState.lampOn = false;
263 digitalWrite(lampPin, LOW);
264 result.changes.add("🔅 Приглушил свет для лучшей видимости проектора");
265 } else if (currentLightPercent < 30) {
266 currentState.lampOn = true;
267 digitalWrite(lampPin, HIGH);
268 result.changes.add("💡 Добавил света - слишком темно");
269 }
270
271 result.changes.add("📽️ Оптимальные условия для презентации готовы");
272
273 return result;
274}
275
276SmartModeResult applyBreakMode(float temp, int light, int sound, bool motion, String timeOfDay) {
277 SmartModeResult result;
278 DynamicJsonDocument changesDoc(512);
279 result.changes = changesDoc.to<JsonArray>();
280
281 result.decision = "Создаю расслабляющую атмосферу для отдыха";
282 result.comfortLevel = 80;
283 result.energySaved = 15;
284
285 // Чуть прохладнее для бодрости
286 if (temp > 23) {
287 currentState.heaterOn = false;
288 digitalWrite(heaterPin, LOW);
289 result.changes.add("❄️ Немного охладил для бодрости");
290 }
291
292 // Мягкое освещение
293 int currentLightPercent = (light * 100) / 4095;
294 if (timeOfDay == "morning" && currentLightPercent < 80) {
295 currentState.lampOn = true;
296 digitalWrite(lampPin, HIGH);
297 result.changes.add("☀️ Добавил света для утренней бодрости");
298 } else if (timeOfDay == "evening" && currentLightPercent > 40) {
299 currentState.lampOn = false;
300 digitalWrite(lampPin, LOW);
301 result.changes.add("🌅 Приглушил свет для расслабления");
302 }
303
304 result.changes.add("☕ Время отдохнуть и восстановиться!");
305
306 return result;
307}
308
309void learnFromUserChoice(String mode, JsonObject sensors) {
310 // Обучение на выборе пользователя
311
312 // Запоминаем, какой режим пользователь выбирает в текущих условиях
313 float temp = sensors["temperature"];
314 int light = sensors["light"];
315
316 if (mode == "comfort") {
317 // Корректируем предпочтения на основе текущих условий
318 if (temp >= 20 && temp <= 24) {
319 userPrefs.comfortTemperature = (userPrefs.comfortTemperature + (int)temp) / 2;
320 }
321
322 int lightPercent = (light * 100) / 4095;
323 if (lightPercent >= 30 && lightPercent <= 80) {
324 user