Персональный образовательный ресурс для Константина на Deepseek API
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

32 KiB

Учебник: Как устроен EduHelper

Это пошаговый учебник для начинающего программиста. Здесь подробно объяснено, как работает каждая часть проекта EduHelper — веб-приложения для обучения с ИИ-ассистентом.


Оглавление

  1. Что такое веб-приложение и как оно устроено
  2. Архитектура: Frontend vs Backend
  3. Backend: Express.js — наш сервер
  4. База данных: SQLite + Prisma
  5. Frontend: React + Vite
  6. Компоненты UI: Shadcn подход
  7. Маршрутизация (роутинг)
  8. Как фронтенд общается с бэкендом
  9. Интеграция с ИИ (DeepSeek)
  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:

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 — это технология, которая позволяет серверу отправлять данные частями по мере их генерации.

Как это работает:

  1. Фронтенд отправляет запрос: «Ответь на вопрос»
  2. Бэкенд начинает получать ответ от ИИ по кусочкам
  3. Каждый кусочек сразу отправляется на фронтенд
  4. Фронтенд показывает текст по мере поступления — как будто ИИ «печатает»
Сервер → Фронтенд:
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;
}

Что происходит:

  1. from — начальное состояние (элемент невидим и смещён вниз на 8px)
  2. to — конечное состояние (полностью виден, на своём месте)
  3. 0.4s — длительность анимации
  4. ease-out — плавное замедление в конце
  5. 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); }
}

Свечение плавно усиливается и ослабевает — создаёт ощущение «дыхания».

Принципы хорошей анимации

  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)

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, ...) гарантирует, что хотя бы одно поле всегда доступно.


Учебник обновлён. Новые разделы: архив с поиском, умное управление полями.