# Учебник: Как устроен EduHelper Это пошаговый учебник для начинающего программиста. Здесь подробно объяснено, как работает каждая часть проекта EduHelper — веб-приложения для обучения с ИИ-ассистентом. --- ## Оглавление 1. [Что такое веб-приложение и как оно устроено](#1-что-такое-веб-приложение) 2. [Архитектура: Frontend vs Backend](#2-архитектура-frontend-vs-backend) 3. [Backend: Express.js — наш сервер](#3-backend-expressjs) 4. [База данных: SQLite + Prisma](#4-база-данных-sqlite--prisma) 5. [Frontend: React + Vite](#5-frontend-react--vite) 6. [Компоненты UI: Shadcn подход](#6-компоненты-ui-shadcn) 7. [Маршрутизация (роутинг)](#7-маршрутизация) 8. [Как фронтенд общается с бэкендом](#8-как-фронтенд-общается-с-бэкендом) 9. [Интеграция с ИИ (DeepSeek)](#9-интеграция-с-ии-deepseek) 10. [Стриминг ответов (SSE)](#10-стриминг-ответов-sse) --- ## 1. Что такое веб-приложение Веб-приложение — это программа, которая работает в браузере. В отличие от обычного сайта, она **интерактивна**: вы можете нажимать кнопки, заполнять формы, получать ответы — всё без перезагрузки страницы. EduHelper — веб-приложение, которое позволяет: - Задавать вопросы ИИ-ассистенту - Генерировать учебники по любой теме - Проходить тесты - Получать ежедневные отчёты об обучении ### Как это работает в общих чертах ``` [Браузер (Chrome)] <--HTTP запросы--> [Сервер (Express)] <--SQL запросы--> [База данных (SQLite)] | v [DeepSeek API] (ИИ в облаке) ``` 1. Вы открываете сайт в браузере 2. Браузер показывает интерфейс (это **frontend**) 3. Когда нужны данные, браузер отправляет запрос на сервер (это **backend**) 4. Сервер достаёт данные из базы данных или обращается к ИИ 5. Сервер отправляет ответ обратно в браузер 6. Браузер обновляет интерфейс --- ## 2. Архитектура: Frontend vs Backend В нашем проекте фронтенд и бэкенд **живут в разных папках** — это называется «разделённая архитектура»: ``` Edu_helper/ ├── backend/ ← Серверная часть (обработка данных, работа с БД и ИИ) ├── frontend/ ← Клиентская часть (интерфейс, кнопки, формы) └── TEXTBOOK.md ← Этот учебник ``` ### Зачем разделять? | Преимущество | Пояснение | |---|---| | **Понятность** | Серверный и клиентский код не перемешаны. Легче найти нужный файл | | **Независимость** | Можно менять фронтенд, не трогая бэкенд, и наоборот | | **Масштабирование** | В будущем можно запустить несколько бэкендов или поменять фронтенд на мобильное приложение | | **Командная работа** | Один разработчик делает фронтенд, другой — бэкенд | ### Как они общаются? Через **HTTP-запросы** (API). Фронтенд отправляет запрос на адрес вроде `http://localhost:3001/api/questions`, а бэкенд возвращает данные в формате JSON. --- ## 3. Backend: Express.js ### Что такое Express.js? Express — это **фреймворк** (набор готовых инструментов) для создания серверов на Node.js. Он позволяет легко описать: «когда приходит запрос на такой-то адрес, делай вот это». ### Структура нашего бэкенда ``` backend/ ├── src/ │ ├── index.ts ← Точка входа. Здесь создаётся и настраивается сервер │ ├── routes/ ← Маршруты (какой запрос куда ведёт) │ │ ├── settings.ts ← /api/settings — настройки приложения │ │ ├── chat.ts ← /api/chat — чат с ИИ │ │ ├── questions.ts ← /api/questions — ежедневные вопросы │ │ ├── textbooks.ts ← /api/textbooks — генерация учебников │ │ ├── tests.ts ← /api/tests — тестирование │ │ └── reports.ts ← /api/reports — ежедневные отчёты │ ├── lib/ ← Вспомогательные модули │ │ ├── prisma.ts ← Подключение к базе данных │ │ └── deepseek.ts ← Подключение к ИИ (DeepSeek) │ └── middleware/ ← Промежуточные обработчики │ └── errorHandler.ts ← Обработка ошибок ├── prisma/ │ └── schema.prisma ← Описание структуры базы данных ├── data/ │ └── edu_helper.db ← Файл базы данных SQLite ├── .env ← Переменные окружения (секретные настройки) ├── tsconfig.json ← Настройки TypeScript └── package.json ← Зависимости проекта и команды запуска ``` ### Как работает маршрут (Route)? Маршрут — это правило: «если пришёл запрос такого-то типа на такой-то адрес, выполни такой-то код». Пример из `routes/settings.ts`: ```typescript router.get("/", async (req, res) => { // GET /api/settings — вернуть все настройки const settings = await prisma.setting.findMany(); res.json(settings); // отправить как JSON }); router.put("/", async (req, res) => { // PUT /api/settings — обновить настройки const entries = req.body; // данные из запроса // ... сохранить в БД ... res.json({ success: true }); }); ``` ### Типы HTTP-запросов | Метод | Для чего | Пример | |-------|---------|--------| | `GET` | Получить данные | Загрузить список вопросов | | `POST` | Создать что-то новое | Сохранить новый вопрос | | `PUT` | Обновить существующее | Изменить настройки | | `DELETE` | Удалить | Очистить историю чата | --- ## 4. База данных: SQLite + Prisma ### Что такое база данных? База данных — это **структурированное хранилище информации**. Представьте таблицу в Excel: | id | text | answer | date | |----|------|--------|------| | 1 | Что такое React? | React — это... | 2026-03-27 | | 2 | Как работает async? | Async позволяет... | 2026-03-27 | Каждая **строка** — это одна запись (вопрос). Каждый **столбец** — это поле (свойство). ### SQLite SQLite — это база данных, которая хранится в **одном файле** (у нас `data/edu_helper.db`). Не нужно ставить отдельную программу — всё работает «из коробки». ### Prisma Prisma — это **ORM** (Object-Relational Mapping). Она позволяет работать с базой данных **на TypeScript** вместо SQL. Без Prisma (чистый SQL): ```sql SELECT * FROM Question WHERE date = '2026-03-27'; ``` С Prisma (TypeScript): ```typescript const questions = await prisma.question.findMany({ where: { date: '2026-03-27' } }); ``` ### Схема базы данных (`schema.prisma`) Схема описывает, какие таблицы и поля есть в нашей БД: ```prisma model Question { id Int @id @default(autoincrement()) // уникальный номер, растёт автоматически text String // текст вопроса answer String? // ответ (? = может быть пустым) date String // дата в формате YYYY-MM-DD createdAt DateTime @default(now()) // дата создания (автоматически) } ``` Наши таблицы: - **Setting** — настройки (API-ключ, промпты) - **ChatMessage** — история чата - **Question** — ежедневные вопросы и ответы - **Textbook** — сгенерированные учебники - **Test** — тесты (вопросы в формате JSON) - **TestResult** — результаты прохождения тестов - **Report** — ежедневные отчёты --- ## 5. Frontend: React + Vite ### Что такое React? React — это библиотека для создания интерфейсов. Главная идея: интерфейс состоит из **компонентов** — переиспользуемых «кирпичиков». Например, кнопка — это компонент. Карточка — компонент. Вся страница — тоже компонент, собранный из других. ### Что такое Vite? Vite — это **сборщик** (build tool). Он берёт все ваши файлы TypeScript, React-компоненты, CSS — и собирает их в один пакет, который понимает браузер. Плюс он поддерживает **горячую перезагрузку** — вы сохраняете файл, и браузер обновляется мгновенно. ### Структура нашего фронтенда ``` frontend/src/ ├── main.tsx ← Точка входа. Монтирует React-приложение в HTML ├── App.tsx ← Корневой компонент с маршрутизацией ├── index.css ← Глобальные стили и тема ├── pages/ ← Страницы приложения (по одной на раздел) │ ├── HomePage.tsx ← Главная: приветствие + чат │ ├── QuestionsPage.tsx ← Ежедневные вопросы │ ├── TextbookPage.tsx ← Генерация учебника │ ├── TestPage.tsx ← Тестирование │ ├── ReportPage.tsx ← Ежедневный отчёт │ └── SettingsPage.tsx ← Настройки (ключ, промпты) ├── components/ ← Переиспользуемые компоненты │ ├── Layout.tsx ← Общий каркас: боковое меню + область контента │ └── ui/ ← Базовые UI-компоненты (кнопки, инпуты, карточки) │ ├── button.tsx │ ├── input.tsx │ ├── textarea.tsx │ ├── card.tsx │ └── label.tsx └── lib/ ← Утилиты └── utils.ts ← Вспомогательные функции (запросы к API, форматирование) ``` ### Как работает React-компонент? Компонент — это функция, которая возвращает **JSX** (разметку, похожую на HTML): ```tsx function Greeting() { const name = "Константин"; return

Привет, {name}!

; } ``` В фигурных скобках `{}` можно писать JavaScript-выражения. ### Состояние (State) Состояние — это данные, которые могут **меняться**. Когда состояние меняется, React перерисовывает компонент. ```tsx const [count, setCount] = useState(0); // count — текущее значение (изначально 0) // setCount — функция для изменения ``` ### Эффекты (useEffect) `useEffect` выполняет код **после** рендера компонента. Используется для загрузки данных, подписок и т.д. ```tsx useEffect(() => { // Этот код выполнится один раз при первом рендере fetch("/api/questions").then(res => res.json()).then(setQuestions); }, []); // пустой массив = выполнить только один раз ``` --- ## 6. Компоненты UI: Shadcn подход ### Что такое Shadcn UI? Shadcn UI — это **не библиотека**, а **коллекция компонентов**, которые вы копируете прямо в свой проект. Это значит: - Вы полностью контролируете код каждого компонента - Можно настроить внешний вид под свои нужды - Нет «чёрного ящика» — всё прозрачно ### Как устроен компонент Button? ```tsx const buttonVariants = cva( "inline-flex items-center justify-center rounded-md text-sm font-medium", { variants: { variant: { default: "bg-primary text-primary-foreground", // синяя кнопка outline: "border border-input bg-background", // кнопка с рамкой ghost: "hover:bg-accent", // прозрачная кнопка }, size: { default: "h-10 px-4 py-2", sm: "h-9 px-3", lg: "h-11 px-8", }, }, } ); ``` `cva` (class-variance-authority) — утилита для создания **вариантов стилей**. Вместо кучи `if/else` для классов CSS, мы декларативно описываем все варианты. ### Tailwind CSS Tailwind — это CSS-фреймворк, где стили задаются через **классы** прямо в HTML: ```html
``` Каждый класс = одно CSS-свойство. Это быстрее писать и легче менять. ### Двухцветная тема По требованию заказчика, дизайн в двух цветах (белый + акцентный). Наши цвета определены в `index.css`: ```css @theme { --color-background: #ffffff; /* белый фон */ --color-primary: #2563eb; /* синий — акцентный цвет */ --color-foreground: #0f172a; /* тёмный текст */ } ``` --- ## 7. Маршрутизация ### Что такое маршрутизация? Когда вы нажимаете на ссылку «Вопросы», URL меняется на `/questions` и показывается нужная страница. Но **страница не перезагружается** — React Router просто меняет компонент. ```tsx }> {/* общий каркас */} } /> } /> } /> } /> } /> } /> ``` `` — это **обёртка**: боковое меню слева, а справа — ``, куда подставляется текущая страница. --- ## 8. Как фронтенд общается с бэкендом ### API-запросы Фронтенд отправляет запросы к бэкенду с помощью функции `fetch`: ```typescript // Утилита из lib/utils.ts async function apiFetch(path: string, options?: RequestInit): Promise { const res = await fetch(`/api${path}`, { headers: { "Content-Type": "application/json" }, ...options, }); return res.json(); } // Использование const questions = await apiFetch("/questions?date=2026-03-27"); ``` ### Прокси в Vite Фронтенд работает на порту `5173`, бэкенд — на `3001`. Чтобы не было проблем с CORS, Vite проксирует запросы: всё, что начинается с `/api`, перенаправляется на бэкенд. ```typescript // vite.config.ts server: { proxy: { "/api": { target: "http://localhost:3001", changeOrigin: true, }, }, } ``` --- ## 9. Интеграция с ИИ (DeepSeek) ### Что такое DeepSeek? DeepSeek — это **языковая модель** (LLM), похожая на ChatGPT. Она принимает текст и генерирует ответ. ### Как мы подключаемся? DeepSeek использует **OpenAI-совместимый API**. Это значит, мы можем использовать библиотеку `openai` от OpenAI, просто поменяв адрес сервера: ```typescript import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api.deepseek.com", // адрес DeepSeek вместо OpenAI apiKey: "ваш-ключ", }); ``` ### Промпты **Промпт** (prompt) — это инструкция для ИИ. Мы используем разные промпты для разных задач: - **prompt_answer** — для ответов на вопросы: «Ответь понятно, используй примеры» - **prompt_textbook** — для учебника: «Объясняй просто, структурируй текст» - **prompt_test** — для тестов: «Сгенерируй 10 вопросов с вариантами ответов» - **prompt_report** — для отчёта: «Составь отчёт в таком-то формате» Все промпты можно редактировать на странице «Настройки». --- ## 10. Стриминг ответов (SSE) ### Проблема Когда ИИ генерирует длинный текст, ожидание может занять 10-30 секунд. Без стриминга пользователь видит пустой экран и думает, что приложение зависло. ### Решение: Server-Sent Events (SSE) SSE — это технология, которая позволяет серверу **отправлять данные частями** по мере их генерации. Как это работает: 1. Фронтенд отправляет запрос: «Ответь на вопрос» 2. Бэкенд начинает получать ответ от ИИ **по кусочкам** 3. Каждый кусочек сразу отправляется на фронтенд 4. Фронтенд показывает текст по мере поступления — как будто ИИ «печатает» ``` Сервер → Фронтенд: data: {"content": "React"} data: {"content": " — это"} data: {"content": " библиотека"} data: {"content": " для"} data: {"content": " создания"} data: {"content": " интерфейсов."} data: [DONE] ``` На бэкенде: ```typescript res.setHeader("Content-Type", "text/event-stream"); const stream = await client.chat.completions.create({ model: "deepseek-chat", messages: [...], stream: true, // ← включаем стриминг }); for await (const chunk of stream) { const content = chunk.choices[0]?.delta?.content || ""; res.write(`data: ${JSON.stringify({ content })}\n\n`); } ``` На фронтенде: ```typescript const reader = res.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const text = decoder.decode(value); // обработать полученные кусочки и обновить UI } ``` --- ## Словарь терминов | Термин | Объяснение | |--------|-----------| | **API** | Application Programming Interface — способ общения между программами | | **HTTP** | Протокол передачи данных в вебе (GET, POST, PUT, DELETE) | | **JSON** | Формат данных: `{"name": "Константин", "age": 25}` | | **REST** | Стиль проектирования API — каждый ресурс имеет свой URL | | **ORM** | Object-Relational Mapping — работа с БД через объекты вместо SQL | | **SSE** | Server-Sent Events — односторонняя отправка данных от сервера к клиенту | | **LLM** | Large Language Model — большая языковая модель (ИИ) | | **Промпт** | Текстовая инструкция для ИИ | | **Компонент** | Переиспользуемый блок интерфейса в React | | **Состояние** | Данные компонента, при изменении которых он перерисовывается | | **Маршрут** | Связь между URL и страницей/обработчиком | --- ## 11. Дизайн: CSS-анимации и визуальные эффекты ### Почему дизайн важен? Интерфейс — это первое, что видит пользователь. Если приложение выглядит современно и приятно, им хочется пользоваться. Claude, ChatGPT и другие ИИ-продукты ставят высокую планку дизайна. ### CSS-анимации Анимации делают интерфейс «живым». В CSS анимация описывается через `@keyframes`: ```css @keyframes fade-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } } .animate-fade-in { animation: fade-in 0.4s ease-out both; } ``` Что происходит: 1. `from` — начальное состояние (элемент невидим и смещён вниз на 8px) 2. `to` — конечное состояние (полностью виден, на своём месте) 3. `0.4s` — длительность анимации 4. `ease-out` — плавное замедление в конце 5. `both` — применить конечное состояние после анимации ### Glow-эффект (свечение) Свечение создаётся через `box-shadow` с цветом акцента: ```css .glow-card:hover { box-shadow: 0 0 0 1px rgba(79, 70, 229, 0.08), /* тонкая рамка */ 0 4px 20px rgba(79, 70, 229, 0.08), /* мягкое свечение */ 0 1px 3px rgba(0, 0, 0, 0.04); /* лёгкая тень */ transform: translateY(-1px); /* лёгкий подъём */ } ``` ### Glassmorphism (стеклянный эффект) Популярный тренд — полупрозрачный фон с размытием: ```css .glass { background: rgba(255, 255, 255, 0.85); /* полупрозрачный белый */ backdrop-filter: blur(12px); /* размытие фона за элементом */ } ``` ### Пульсирующее свечение Для привлечения внимания (например, иконка логотипа): ```css @keyframes pulse-glow { 0%, 100% { box-shadow: 0 0 15px rgba(79, 70, 229, 0.15); } 50% { box-shadow: 0 0 30px rgba(79, 70, 229, 0.25); } } ``` Свечение плавно усиливается и ослабевает — создаёт ощущение «дыхания». ### Принципы хорошей анимации 1. **Быстро** — не более 0.3-0.5 секунды (длиннее раздражает) 2. **Плавно** — используйте `ease-out` или `cubic-bezier` 3. **Ненавязчиво** — анимация не должна мешать работе 4. **Осмысленно** — анимируйте только то, что привлекает внимание к действию --- ## 12. Electron: веб-приложение как десктоп-программа ### Что такое Electron? Electron — это фреймворк, который позволяет запускать веб-приложение **как обычную программу** на Windows, macOS и Linux. Технически, он встраивает браузер Chromium внутрь окна приложения. Пример приложений на Electron: VS Code, Discord, Slack, Notion. ### Как это работает ``` ┌──────────────────────────────────┐ │ Окно Electron (Chromium) │ │ ┌────────────────────────────┐ │ │ │ Наш React-фронтенд │ │ │ │ (http://localhost:5173) │ │ │ └────────────────────────────┘ │ │ │ │ Node.js (запускает бэкенд) │ └──────────────────────────────────┘ ``` 1. Electron запускает **бэкенд** (Express-сервер) как дочерний процесс 2. Открывает **окно** и загружает в него фронтенд 3. Пользователь видит обычное приложение, без адресной строки браузера ### Главный файл Electron (`electron/main.js`) ```javascript const { app, BrowserWindow } = require("electron"); function createWindow() { const win = new BrowserWindow({ width: 1200, height: 800, titleBarStyle: "hiddenInset", // macOS-стиль: кнопки светофора встроены }); win.loadURL("http://localhost:5173"); } app.whenReady().then(createWindow); ``` ### Команды запуска ```bash # Как десктоп-программу (Electron + бэкенд + фронтенд): npm run dev # Как обычное веб-приложение в браузере: npm run dev:web ``` --- ## Словарь терминов | Термин | Объяснение | |--------|-----------| | **API** | Application Programming Interface — способ общения между программами | | **HTTP** | Протокол передачи данных в вебе (GET, POST, PUT, DELETE) | | **JSON** | Формат данных: `{"name": "Константин", "age": 25}` | | **REST** | Стиль проектирования API — каждый ресурс имеет свой URL | | **ORM** | Object-Relational Mapping — работа с БД через объекты вместо SQL | | **SSE** | Server-Sent Events — односторонняя отправка данных от сервера к клиенту | | **LLM** | Large Language Model — большая языковая модель (ИИ) | | **Промпт** | Текстовая инструкция для ИИ | | **Компонент** | Переиспользуемый блок интерфейса в React | | **Состояние** | Данные компонента, при изменении которых он перерисовывается | | **Маршрут** | Связь между URL и страницей/обработчиком | | **Electron** | Фреймворк для создания десктоп-приложений из веб-технологий | | **Glassmorphism** | Дизайн-тренд: полупрозрачные элементы с размытием фона | | **@keyframes** | CSS-конструкция для описания шагов анимации | --- ## 13. Архив и поиск: работа с накопленными данными ### Зачем нужен архив? Со временем в приложении накапливаются данные: вопросы, учебники, отчёты. Без архива старые данные «теряются» — их не видно на текущей странице. Архив собирает **всё** в одном месте. ### Группировка по датам Данные в архиве организованы по дням. Для этого мы берём массив объектов и группируем: ```typescript function groupByDate(items) { const groups = {}; for (const item of items) { const date = item.createdAt.split("T")[0]; // "2026-03-27T15:30:00" → "2026-03-27" if (!groups[date]) groups[date] = []; groups[date].push(item); } return groups; // Результат: { "2026-03-27": [...], "2026-03-26": [...] } } ``` ### Поиск (фильтрация) Поиск на фронтенде — это **фильтрация** массива по введённому тексту: ```typescript const filtered = questions.filter( (q) => q.text.toLowerCase().includes(search.toLowerCase()) ); ``` `includes()` проверяет, содержит ли строка подстроку. `toLowerCase()` делает поиск нечувствительным к регистру. ### Табы (вкладки) Табы — это состояние, определяющее какой контент показывать: ```typescript const [tab, setTab] = useState("questions"); // "questions" | "textbooks" | "reports" // В зависимости от tab показываем разные списки {tab === "questions" && } {tab === "textbooks" && } ``` --- ## 14. Умное управление полями ввода ### Задача По ТЗ нужно заполнить **минимум 5 вопросов в день**. Но если 3 уже сохранены — показывать 5 полей глупо. Нужно показать только 2 оставшихся (плюс кнопку «Ещё»). ### Решение ```typescript function updateFieldCount(savedCount: number) { const needed = Math.max(1, 5 - savedCount); // Если сохранено 0 → 5 полей // Если сохранено 3 → 2 поля // Если сохранено 5+ → 1 поле (всегда можно добавить ещё) setFields(Array(needed).fill("")); } ``` `Math.max(1, ...)` гарантирует, что **хотя бы одно поле** всегда доступно. --- *Учебник обновлён. Новые разделы: архив с поиском, умное управление полями.*