# Учебник: Как устроен 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
```
Каждый класс = одно 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, ...)` гарантирует, что **хотя бы одно поле** всегда доступно.
---
*Учебник обновлён. Новые разделы: архив с поиском, умное управление полями.*