32 KiB
Учебник: Как устроен EduHelper
Это пошаговый учебник для начинающего программиста. Здесь подробно объяснено, как работает каждая часть проекта EduHelper — веб-приложения для обучения с ИИ-ассистентом.
Оглавление
- Что такое веб-приложение и как оно устроено
- Архитектура: Frontend vs Backend
- Backend: Express.js — наш сервер
- База данных: SQLite + Prisma
- Frontend: React + Vite
- Компоненты UI: Shadcn подход
- Маршрутизация (роутинг)
- Как фронтенд общается с бэкендом
- Интеграция с ИИ (DeepSeek)
- Стриминг ответов (SSE)
1. Что такое веб-приложение
Веб-приложение — это программа, которая работает в браузере. В отличие от обычного сайта, она интерактивна: вы можете нажимать кнопки, заполнять формы, получать ответы — всё без перезагрузки страницы.
EduHelper — веб-приложение, которое позволяет:
- Задавать вопросы ИИ-ассистенту
- Генерировать учебники по любой теме
- Проходить тесты
- Получать ежедневные отчёты об обучении
Как это работает в общих чертах
[Браузер (Chrome)] <--HTTP запросы--> [Сервер (Express)] <--SQL запросы--> [База данных (SQLite)]
|
v
[DeepSeek API] (ИИ в облаке)
- Вы открываете сайт в браузере
- Браузер показывает интерфейс (это frontend)
- Когда нужны данные, браузер отправляет запрос на сервер (это backend)
- Сервер достаёт данные из базы данных или обращается к ИИ
- Сервер отправляет ответ обратно в браузер
- Браузер обновляет интерфейс
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:
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):
SELECT * FROM Question WHERE date = '2026-03-27';
С Prisma (TypeScript):
const questions = await prisma.question.findMany({
where: { date: '2026-03-27' }
});
Схема базы данных (schema.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):
function Greeting() {
const name = "Константин";
return <h1>Привет, {name}!</h1>;
}
В фигурных скобках {} можно писать JavaScript-выражения.
Состояние (State)
Состояние — это данные, которые могут меняться. Когда состояние меняется, React перерисовывает компонент.
const [count, setCount] = useState(0);
// count — текущее значение (изначально 0)
// setCount — функция для изменения
<button onClick={() => setCount(count + 1)}>
Нажали {count} раз
</button>
Эффекты (useEffect)
useEffect выполняет код после рендера компонента. Используется для загрузки данных, подписок и т.д.
useEffect(() => {
// Этот код выполнится один раз при первом рендере
fetch("/api/questions").then(res => res.json()).then(setQuestions);
}, []); // пустой массив = выполнить только один раз
6. Компоненты UI: Shadcn подход
Что такое Shadcn UI?
Shadcn UI — это не библиотека, а коллекция компонентов, которые вы копируете прямо в свой проект. Это значит:
- Вы полностью контролируете код каждого компонента
- Можно настроить внешний вид под свои нужды
- Нет «чёрного ящика» — всё прозрачно
Как устроен компонент Button?
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:
<!-- Обычный CSS -->
<div style="display: flex; padding: 16px; background: white; border-radius: 8px;">
<!-- Tailwind CSS -->
<div class="flex p-4 bg-white rounded-lg">
Каждый класс = одно CSS-свойство. Это быстрее писать и легче менять.
Двухцветная тема
По требованию заказчика, дизайн в двух цветах (белый + акцентный). Наши цвета определены в index.css:
@theme {
--color-background: #ffffff; /* белый фон */
--color-primary: #2563eb; /* синий — акцентный цвет */
--color-foreground: #0f172a; /* тёмный текст */
}
7. Маршрутизация
Что такое маршрутизация?
Когда вы нажимаете на ссылку «Вопросы», URL меняется на /questions и показывается нужная страница. Но страница не перезагружается — React Router просто меняет компонент.
<Routes>
<Route element={<Layout />}> {/* общий каркас */}
<Route path="/" element={<HomePage />} />
<Route path="/questions" element={<QuestionsPage />} />
<Route path="/textbook" element={<TextbookPage />} />
<Route path="/test" element={<TestPage />} />
<Route path="/report" element={<ReportPage />} />
<Route path="/settings" element={<SettingsPage />} />
</Route>
</Routes>
<Layout /> — это обёртка: боковое меню слева, а справа — <Outlet />, куда подставляется текущая страница.
8. Как фронтенд общается с бэкендом
API-запросы
Фронтенд отправляет запросы к бэкенду с помощью функции fetch:
// Утилита из lib/utils.ts
async function apiFetch<T>(path: string, options?: RequestInit): Promise<T> {
const res = await fetch(`/api${path}`, {
headers: { "Content-Type": "application/json" },
...options,
});
return res.json();
}
// Использование
const questions = await apiFetch<Question[]>("/questions?date=2026-03-27");
Прокси в Vite
Фронтенд работает на порту 5173, бэкенд — на 3001. Чтобы не было проблем с CORS, Vite проксирует запросы: всё, что начинается с /api, перенаправляется на бэкенд.
// vite.config.ts
server: {
proxy: {
"/api": {
target: "http://localhost:3001",
changeOrigin: true,
},
},
}
9. Интеграция с ИИ (DeepSeek)
Что такое DeepSeek?
DeepSeek — это языковая модель (LLM), похожая на ChatGPT. Она принимает текст и генерирует ответ.
Как мы подключаемся?
DeepSeek использует OpenAI-совместимый API. Это значит, мы можем использовать библиотеку openai от OpenAI, просто поменяв адрес сервера:
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 — это технология, которая позволяет серверу отправлять данные частями по мере их генерации.
Как это работает:
- Фронтенд отправляет запрос: «Ответь на вопрос»
- Бэкенд начинает получать ответ от ИИ по кусочкам
- Каждый кусочек сразу отправляется на фронтенд
- Фронтенд показывает текст по мере поступления — как будто ИИ «печатает»
Сервер → Фронтенд:
data: {"content": "React"}
data: {"content": " — это"}
data: {"content": " библиотека"}
data: {"content": " для"}
data: {"content": " создания"}
data: {"content": " интерфейсов."}
data: [DONE]
На бэкенде:
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`);
}
На фронтенде:
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:
@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;
}
Что происходит:
from— начальное состояние (элемент невидим и смещён вниз на 8px)to— конечное состояние (полностью виден, на своём месте)0.4s— длительность анимацииease-out— плавное замедление в концеboth— применить конечное состояние после анимации
Glow-эффект (свечение)
Свечение создаётся через box-shadow с цветом акцента:
.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 (стеклянный эффект)
Популярный тренд — полупрозрачный фон с размытием:
.glass {
background: rgba(255, 255, 255, 0.85); /* полупрозрачный белый */
backdrop-filter: blur(12px); /* размытие фона за элементом */
}
Пульсирующее свечение
Для привлечения внимания (например, иконка логотипа):
@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); }
}
Свечение плавно усиливается и ослабевает — создаёт ощущение «дыхания».
Принципы хорошей анимации
- Быстро — не более 0.3-0.5 секунды (длиннее раздражает)
- Плавно — используйте
ease-outилиcubic-bezier - Ненавязчиво — анимация не должна мешать работе
- Осмысленно — анимируйте только то, что привлекает внимание к действию
12. Electron: веб-приложение как десктоп-программа
Что такое Electron?
Electron — это фреймворк, который позволяет запускать веб-приложение как обычную программу на Windows, macOS и Linux. Технически, он встраивает браузер Chromium внутрь окна приложения.
Пример приложений на Electron: VS Code, Discord, Slack, Notion.
Как это работает
┌──────────────────────────────────┐
│ Окно Electron (Chromium) │
│ ┌────────────────────────────┐ │
│ │ Наш React-фронтенд │ │
│ │ (http://localhost:5173) │ │
│ └────────────────────────────┘ │
│ │
│ Node.js (запускает бэкенд) │
└──────────────────────────────────┘
- Electron запускает бэкенд (Express-сервер) как дочерний процесс
- Открывает окно и загружает в него фронтенд
- Пользователь видит обычное приложение, без адресной строки браузера
Главный файл Electron (electron/main.js)
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);
Команды запуска
# Как десктоп-программу (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. Архив и поиск: работа с накопленными данными
Зачем нужен архив?
Со временем в приложении накапливаются данные: вопросы, учебники, отчёты. Без архива старые данные «теряются» — их не видно на текущей странице. Архив собирает всё в одном месте.
Группировка по датам
Данные в архиве организованы по дням. Для этого мы берём массив объектов и группируем:
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": [...] }
}
Поиск (фильтрация)
Поиск на фронтенде — это фильтрация массива по введённому тексту:
const filtered = questions.filter(
(q) => q.text.toLowerCase().includes(search.toLowerCase())
);
includes() проверяет, содержит ли строка подстроку. toLowerCase() делает поиск нечувствительным к регистру.
Табы (вкладки)
Табы — это состояние, определяющее какой контент показывать:
const [tab, setTab] = useState("questions"); // "questions" | "textbooks" | "reports"
// В зависимости от tab показываем разные списки
{tab === "questions" && <QuestionsList />}
{tab === "textbooks" && <TextbooksList />}
14. Умное управление полями ввода
Задача
По ТЗ нужно заполнить минимум 5 вопросов в день. Но если 3 уже сохранены — показывать 5 полей глупо. Нужно показать только 2 оставшихся (плюс кнопку «Ещё»).
Решение
function updateFieldCount(savedCount: number) {
const needed = Math.max(1, 5 - savedCount);
// Если сохранено 0 → 5 полей
// Если сохранено 3 → 2 поля
// Если сохранено 5+ → 1 поле (всегда можно добавить ещё)
setFields(Array(needed).fill(""));
}
Math.max(1, ...) гарантирует, что хотя бы одно поле всегда доступно.
Учебник обновлён. Новые разделы: архив с поиском, умное управление полями.