Skip to main content

Sprint 18 Extra

📱 СПРИНТ 18: “ИНТЕРАКТИВНЫЙ ВЕБ-ИНТЕРФЕЙС УПРАВЛЕНИЯ”

От статического сайта к живому цифровому опыту


🎯 МЕТОДОЛОГИЧЕСКАЯ КОНЦЕПЦИЯ СПРИНТА

Философия перехода:

1БЫЛО: Веб-страница показывает данные (пассивное потребление)
2СТАЛО: Веб-интерфейс = живой организм (активное взаимодействие)

Ключевая идея: Дети создают “цифрового собеседника” - интерфейс, который не просто показывает информацию, а ведет диалог с пользователем, предугадывает потребности и адаптируется под каждого человека.

Концептуальная эволюция:

  • Статика → Динамика: От показа данных к интерактивному опыту
  • Монолог → Диалог: Система не просто говорит, но и слушает
  • Универсальность → Персонализация: Интерфейс адаптируется под пользователя
  • Инструмент → Партнер: Система становится цифровым помощником

🧠 ПЕДАГОГИЧЕСКИЕ ЦЕЛИ СПРИНТА

Концептуальные цели:

  • Понимание “интерактивности” как двустороннего общения
  • Осознание “адаптивности” интерфейсов под пользователя
  • Введение понятий “UX/UI дизайна” - психология взаимодействия
  • Понимание “персонализации” как основы современных систем

Технические цели:

  • Продвинутый JavaScript для интерактивности
  • CSS анимации и переходы для живости интерфейса
  • Drag & Drop, жесты, голосовое управление
  • Адаптивный дизайн для разных устройств
  • Локальное хранение пользовательских настроек

Метакогнитивные цели:

  • “Эмпатическое мышление” - понимание потребностей пользователя
  • “Дизайн-мышление” - итеративное улучшение опыта
  • “Поведенческое мышление” - как интерфейс влияет на действия

📚 СТРУКТУРА СПРИНТА (4 занятия)

Занятие 1: “Психология взаимодействия” 🧠

Длительность: 90 минут

Фаза 1: Философия интерактивности (20 мин)

Метод: Экспериментальная психология

Эксперимент “Разные способы общения”:

  1. Бумажная записка (статический веб-сайт)

    • Учитель пишет на доске: “Температура: 22°C”
    • Ученики могут только смотреть
  2. Разговор через переводчика (API)

    • Ученик → переводчик → учитель → переводчик → ученик
    • “Какая температура?” → “22°C”
  3. Живое общение (интерактивный интерфейс)

    • Прямой диалог, жесты, мимика, адаптация

Ключевые открытия:

  • “Интерактивность = способность системы учиться от пользователя”
  • “Хороший интерфейс предугадывает желания”
  • “Система должна адаптироваться под человека, а не наоборот”

Фаза 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. Принцип отзывчивости: “Система всегда отвечает на действие”
  2. Принцип предсказуемости: “Похожие действия дают похожие результаты”
  3. Принцип обратной связи: “Пользователь всегда знает, что происходит”
  4. Принцип прощения: “Можно отменить любое действие”
  5. Принцип доступности: “Каждый может пользоваться системой”
  6. Принцип эффективности: “Частые действия должны быть простыми”
  7. Принцип красоты: “Приятный вид = желание пользоваться”
  8. Принцип персонализации: “Система помнит предпочтения”
  9. Принцип контекста: “Интерфейс адаптируется под ситуацию”
  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