Files
Константин Лебединский 3dec3ea720 Initial commit: Edu Helper (Docker, React, Express, Prisma)
Made-with: Cursor
2026-04-01 21:40:51 +05:00

731 lines
32 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Учебник: Как устроен 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 <h1>Привет, {name}!</h1>;
}
```
В фигурных скобках `{}` можно писать JavaScript-выражения.
### Состояние (State)
Состояние — это данные, которые могут **меняться**. Когда состояние меняется, React перерисовывает компонент.
```tsx
const [count, setCount] = useState(0);
// count — текущее значение (изначально 0)
// setCount — функция для изменения
<button onClick={() => setCount(count + 1)}>
Нажали {count} раз
</button>
```
### Эффекты (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 -->
<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`:
```css
@theme {
--color-background: #ffffff; /* белый фон */
--color-primary: #2563eb; /* синий — акцентный цвет */
--color-foreground: #0f172a; /* тёмный текст */
}
```
---
## 7. Маршрутизация
### Что такое маршрутизация?
Когда вы нажимаете на ссылку «Вопросы», URL меняется на `/questions` и показывается нужная страница. Но **страница не перезагружается** — React Router просто меняет компонент.
```tsx
<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`:
```typescript
// Утилита из 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`, перенаправляется на бэкенд.
```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" && <QuestionsList />}
{tab === "textbooks" && <TextbooksList />}
```
---
## 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, ...)` гарантирует, что **хотя бы одно поле** всегда доступно.
---
*Учебник обновлён. Новые разделы: архив с поиском, умное управление полями.*