challenge-admin-pl/docs/CHALLENGE_FRONTEND_GUIDE.md
Primakov Alexandr Alexandrovich e777b57991 init + api use
2025-11-03 17:59:08 +03:00

1126 lines
33 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.

# Challenge Service - Руководство для фронтенда
Руководство по интеграции сервиса проверки заданий через LLM в пользовательский интерфейс.
## Содержание
1. [Основные сценарии использования](#основные-сценарии-использования)
2. [Структура данных](#структура-данных)
3. [API взаимодействие](#api-взаимодействие)
4. [Компоненты UI](#компоненты-ui)
5. [State Management](#state-management)
6. [Best Practices](#best-practices)
---
## Основные сценарии использования
### 1. Сценарий для студента
```
┌─────────────────────────────────────────────────────────────┐
│ 1. Вход/Регистрация (nickname) │
│ POST /api/challenge/auth │
│ → Сохранить userId в localStorage/state │
└────────────────┬────────────────────────────────────────────┘
┌────────────────▼────────────────────────────────────────────┐
│ 2. Просмотр доступных цепочек заданий │
│ GET /api/challenge/chains │
│ → Отобразить список с прогрессом │
└────────────────┬────────────────────────────────────────────┘
┌────────────────▼────────────────────────────────────────────┐
│ 3. Выбор задания из цепочки │
│ → Отобразить описание (Markdown) │
│ → Показать историю своих попыток │
└────────────────┬────────────────────────────────────────────┘
┌────────────────▼────────────────────────────────────────────┐
│ 4. Написание решения │
│ → Текстовое поле / Code Editor │
│ → Кнопка "Отправить на проверку" │
└────────────────┬────────────────────────────────────────────┘
┌────────────────▼────────────────────────────────────────────┐
│ 5. Отправка на проверку │
│ POST /api/challenge/submit │
│ → Получить queueId │
└────────────────┬────────────────────────────────────────────┘
┌────────────────▼────────────────────────────────────────────┐
│ 6. Ожидание результата (polling) │
│ GET /api/challenge/check-status/:queueId │
│ → Показать индикатор загрузки + позицию в очереди │
│ → Повторять каждые 2-3 секунды │
└────────────────┬────────────────────────────────────────────┘
┌────────────────▼────────────────────────────────────────────┐
│ 7. Получение результата │
│ ✅ ПРИНЯТО → Поздравление + переход к следующему │
│ ❌ ДОРАБОТКА → Feedback от LLM + возможность повторить │
└─────────────────────────────────────────────────────────────┘
```
### 2. Сценарий для администратора
```
┌─────────────────────────────────────────────────────────────┐
│ 1. Создание заданий │
│ POST /api/challenge/task │
│ → Markdown редактор для описания │
└────────────────┬────────────────────────────────────────────┘
┌────────────────▼────────────────────────────────────────────┐
│ 2. Создание цепочек │
│ POST /api/challenge/chain │
│ → Выбор заданий + порядок │
└────────────────┬────────────────────────────────────────────┘
┌────────────────▼────────────────────────────────────────────┐
│ 3. Мониторинг статистики │
│ GET /api/challenge/stats │
│ → Dashboard с метриками │
└─────────────────────────────────────────────────────────────┘
```
---
## Структура данных
### User (Пользователь)
```typescript
interface ChallengeUser {
_id: string
id: string
nickname: string
createdAt: string // ISO 8601
}
```
### Task (Задание)
```typescript
interface ChallengeTask {
_id: string
id: string
title: string
description: string // Markdown (видно всем)
hiddenInstructions?: string // Скрытые инструкции для LLM (только для преподавателей)
creator?: object // Данные создателя (только для преподавателей)
createdAt: string
updatedAt: string
}
```
**Важно:** Поля `hiddenInstructions` и `creator` доступны только пользователям с ролью `teacher` в Keycloak. При запросе заданий обычными студентами эти поля будут отфильтрованы на сервере.
### Chain (Цепочка заданий)
```typescript
interface ChallengeChain {
_id: string
id: string
name: string
tasks: ChallengeTask[] // Populated
createdAt: string
updatedAt: string
}
```
### Submission (Попытка)
```typescript
type SubmissionStatus = 'pending' | 'in_progress' | 'accepted' | 'needs_revision'
interface ChallengeSubmission {
_id: string
id: string
user: ChallengeUser | string // Populated или ID
task: ChallengeTask | string // Populated или ID
result: string // Результат пользователя
status: SubmissionStatus
queueId?: string
feedback?: string // Комментарий от LLM
submittedAt: string
checkedAt?: string
attemptNumber: number
}
```
### Queue Status (Статус проверки)
```typescript
type QueueStatusType = 'waiting' | 'in_progress' | 'completed' | 'error' | 'not_found'
interface QueueStatus {
status: QueueStatusType
submission?: ChallengeSubmission
error?: string
position?: number // Позиция в очереди (если waiting)
}
```
### User Stats (Статистика пользователя)
```typescript
interface TaskStats {
taskId: string
taskTitle: string
attempts: Array<{
attemptNumber: number
status: SubmissionStatus
submittedAt: string
checkedAt?: string
feedback?: string
}>
totalAttempts: number
status: 'not_attempted' | 'pending' | 'in_progress' | 'completed' | 'needs_revision'
lastAttemptAt: string | null
}
interface ChainStats {
chainId: string
chainName: string
totalTasks: number
completedTasks: number
progress: number // 0-100
}
interface UserStats {
totalTasksAttempted: number
completedTasks: number
inProgressTasks: number
needsRevisionTasks: number
totalSubmissions: number
averageCheckTimeMs: number
taskStats: TaskStats[]
chainStats: ChainStats[]
}
```
### System Stats (Общая статистика)
```typescript
interface SystemStats {
users: number
tasks: number
chains: number
submissions: {
total: number
accepted: number
rejected: number
pending: number
inProgress: number
}
averageCheckTimeMs: number
queue: {
queueLength: number
waiting: number
inProgress: number
maxConcurrency: number
currentlyProcessing: number
}
}
```
---
## API взаимодействие
### Базовая настройка
```typescript
const API_BASE_URL = 'http://localhost:8082/api/challenge'
// Универсальная функция для запросов
async function apiRequest<T>(
endpoint: string,
options: RequestInit = {}
): Promise<{ error: any; data: T }> {
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
headers: {
'Content-Type': 'application/json',
...options.headers,
},
...options,
})
return response.json()
}
```
### Примеры использования
#### 1. Аутентификация
```typescript
async function authenticateUser(nickname: string): Promise<string> {
const { data, error } = await apiRequest<{ ok: boolean; userId: string }>(
'/auth',
{
method: 'POST',
body: JSON.stringify({ nickname }),
}
)
if (error) throw error
// Сохраняем userId
localStorage.setItem('challengeUserId', data.userId)
return data.userId
}
```
#### 2. Получение цепочек
```typescript
async function getChains(): Promise<ChallengeChain[]> {
const { data, error } = await apiRequest<ChallengeChain[]>('/chains')
if (error) throw error
return data
}
```
#### 3. Отправка решения
```typescript
async function submitSolution(
userId: string,
taskId: string,
result: string
): Promise<{ queueId: string; submissionId: string }> {
const { data, error } = await apiRequest<{
queueId: string
submissionId: string
}>('/submit', {
method: 'POST',
body: JSON.stringify({ userId, taskId, result }),
})
if (error) throw error
return data
}
```
#### 4. Polling проверки
```typescript
async function pollCheckStatus(
queueId: string,
onUpdate: (status: QueueStatus) => void,
interval: number = 2000
): Promise<ChallengeSubmission> {
return new Promise((resolve, reject) => {
const checkStatus = async () => {
try {
const { data, error } = await apiRequest<QueueStatus>(
`/check-status/${queueId}`
)
if (error) {
reject(error)
return
}
onUpdate(data)
if (data.status === 'completed' && data.submission) {
resolve(data.submission)
} else if (data.status === 'error') {
reject(new Error(data.error || 'Check failed'))
} else {
// Продолжаем polling
setTimeout(checkStatus, interval)
}
} catch (err) {
reject(err)
}
}
checkStatus()
})
}
```
#### 5. Получение статистики пользователя
```typescript
async function getUserStats(userId: string): Promise<UserStats> {
const { data, error } = await apiRequest<UserStats>(`/user/${userId}/stats`)
if (error) throw error
return data
}
```
---
## Компоненты UI
### 1. AuthForm - Форма входа
```typescript
// AuthForm.tsx
import { useState } from 'react'
interface AuthFormProps {
onAuth: (userId: string, nickname: string) => void
}
export function AuthForm({ onAuth }: AuthFormProps) {
const [nickname, setNickname] = useState('')
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
setError('')
try {
const userId = await authenticateUser(nickname)
onAuth(userId, nickname)
} catch (err) {
setError('Ошибка входа. Попробуйте другой nickname.')
} finally {
setLoading(false)
}
}
return (
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="Введите ваш nickname"
value={nickname}
onChange={(e) => setNickname(e.target.value)}
minLength={3}
maxLength={50}
required
/>
<button type="submit" disabled={loading}>
{loading ? 'Вход...' : 'Войти'}
</button>
{error && <div className="error">{error}</div>}
</form>
)
}
```
### 2. ChainList - Список цепочек
```typescript
// ChainList.tsx
import { useState, useEffect } from 'react'
interface ChainListProps {
userId: string
onSelectChain: (chain: ChallengeChain) => void
}
export function ChainList({ userId, onSelectChain }: ChainListProps) {
const [chains, setChains] = useState<ChallengeChain[]>([])
const [stats, setStats] = useState<UserStats | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
loadData()
}, [userId])
const loadData = async () => {
try {
const [chainsData, statsData] = await Promise.all([
getChains(),
getUserStats(userId)
])
setChains(chainsData)
setStats(statsData)
} catch (err) {
console.error(err)
} finally {
setLoading(false)
}
}
const getChainProgress = (chainId: string) => {
return stats?.chainStats.find(cs => cs.chainId === chainId)
}
if (loading) return <div>Загрузка...</div>
return (
<div className="chain-list">
{chains.map(chain => {
const progress = getChainProgress(chain.id)
return (
<div key={chain.id} className="chain-card" onClick={() => onSelectChain(chain)}>
<h3>{chain.name}</h3>
<p>{chain.tasks.length} заданий</p>
{progress && (
<div className="progress">
<div className="progress-bar" style={{ width: `${progress.progress}%` }} />
<span>{progress.completedTasks} / {progress.totalTasks}</span>
</div>
)}
</div>
)
})}
</div>
)
}
```
### 3. TaskView - Просмотр и решение задания
```typescript
// TaskView.tsx
import { useState } from 'react'
import ReactMarkdown from 'react-markdown'
interface TaskViewProps {
task: ChallengeTask
userId: string
onComplete: () => void
}
export function TaskView({ task, userId, onComplete }: TaskViewProps) {
const [result, setResult] = useState('')
const [submitting, setSubmitting] = useState(false)
const handleSubmit = async () => {
setSubmitting(true)
try {
const { queueId } = await submitSolution(userId, task.id, result)
// Переходим к экрану проверки
// (см. CheckStatusView)
} catch (err) {
alert('Ошибка отправки')
} finally {
setSubmitting(false)
}
}
return (
<div className="task-view">
<h2>{task.title}</h2>
<div className="task-description">
<ReactMarkdown>{task.description}</ReactMarkdown>
</div>
<div className="solution-editor">
<h3>Ваше решение:</h3>
<textarea
value={result}
onChange={(e) => setResult(e.target.value)}
placeholder="Напишите ваше решение здесь..."
rows={15}
/>
</div>
<button
onClick={handleSubmit}
disabled={!result.trim() || submitting}
>
{submitting ? 'Отправка...' : 'Отправить на проверку'}
</button>
</div>
)
}
```
### 4. CheckStatusView - Отслеживание проверки
```typescript
// CheckStatusView.tsx
import { useState, useEffect } from 'react'
interface CheckStatusViewProps {
queueId: string
onComplete: (submission: ChallengeSubmission) => void
}
export function CheckStatusView({ queueId, onComplete }: CheckStatusViewProps) {
const [status, setStatus] = useState<QueueStatus | null>(null)
const [error, setError] = useState('')
useEffect(() => {
pollCheckStatus(queueId, setStatus)
.then(onComplete)
.catch((err) => setError(err.message))
}, [queueId])
if (error) {
return (
<div className="check-error">
<h3> Ошибка проверки</h3>
<p>{error}</p>
</div>
)
}
if (!status) {
return <div>Инициализация...</div>
}
return (
<div className="check-status">
{status.status === 'waiting' && (
<>
<div className="spinner" />
<h3> Ожидание в очереди</h3>
{status.position && <p>Позиция в очереди: {status.position}</p>}
</>
)}
{status.status === 'in_progress' && (
<>
<div className="spinner" />
<h3>🔍 Проверяем ваше решение...</h3>
<p>Это может занять несколько секунд</p>
</>
)}
</div>
)
}
```
### 5. ResultView - Отображение результата
```typescript
// ResultView.tsx
interface ResultViewProps {
submission: ChallengeSubmission
onRetry?: () => void
onNext?: () => void
}
export function ResultView({ submission, onRetry, onNext }: ResultViewProps) {
const isAccepted = submission.status === 'accepted'
return (
<div className={`result-view ${isAccepted ? 'accepted' : 'needs-revision'}`}>
<div className="result-icon">
{isAccepted ? '✅' : '❌'}
</div>
<h2>{isAccepted ? 'Задание принято!' : 'Требуется доработка'}</h2>
<div className="feedback">
<h3>Комментарий:</h3>
<p>{submission.feedback}</p>
</div>
<div className="result-meta">
<p>Попытка #{submission.attemptNumber}</p>
<p>Время проверки: {getCheckTime(submission)}</p>
</div>
<div className="actions">
{isAccepted && onNext && (
<button onClick={onNext} className="btn-primary">
Следующее задание
</button>
)}
{!isAccepted && onRetry && (
<button onClick={onRetry} className="btn-secondary">
Попробовать снова
</button>
)}
</div>
</div>
)
}
function getCheckTime(submission: ChallengeSubmission): string {
if (!submission.checkedAt) return 'N/A'
const submitted = new Date(submission.submittedAt)
const checked = new Date(submission.checkedAt)
const diff = checked.getTime() - submitted.getTime()
return `${Math.round(diff / 1000)} сек`
}
```
### 6. UserStatsView - Статистика пользователя
```typescript
// UserStatsView.tsx
import { useState, useEffect } from 'react'
interface UserStatsViewProps {
userId: string
}
export function UserStatsView({ userId }: UserStatsViewProps) {
const [stats, setStats] = useState<UserStats | null>(null)
useEffect(() => {
getUserStats(userId).then(setStats)
}, [userId])
if (!stats) return <div>Загрузка...</div>
return (
<div className="user-stats">
<h2>Ваша статистика</h2>
<div className="stats-overview">
<div className="stat-card">
<h3>{stats.completedTasks}</h3>
<p>Задания выполнено</p>
</div>
<div className="stat-card">
<h3>{stats.totalTasksAttempted}</h3>
<p>Всего попыток</p>
</div>
<div className="stat-card">
<h3>{Math.round(stats.averageCheckTimeMs / 1000)}с</h3>
<p>Среднее время проверки</p>
</div>
</div>
<h3>Прогресс по цепочкам</h3>
{stats.chainStats.map(chain => (
<div key={chain.chainId} className="chain-progress">
<h4>{chain.chainName}</h4>
<div className="progress-bar">
<div style={{ width: `${chain.progress}%` }} />
</div>
<span>{chain.completedTasks} / {chain.totalTasks}</span>
</div>
))}
<h3>Детали по заданиям</h3>
{stats.taskStats.map(taskStat => (
<div key={taskStat.taskId} className="task-stat">
<h4>{taskStat.taskTitle}</h4>
<p>Статус: {getStatusLabel(taskStat.status)}</p>
<p>Попыток: {taskStat.totalAttempts}</p>
</div>
))}
</div>
)
}
function getStatusLabel(status: string): string {
const labels: Record<string, string> = {
'not_attempted': 'Не начато',
'pending': 'Ожидает проверки',
'in_progress': 'Проверяется',
'completed': 'Завершено',
'needs_revision': 'Требует доработки'
}
return labels[status] || status
}
```
### 7. AdminDashboard - Панель администратора
```typescript
// AdminDashboard.tsx
import { useState, useEffect } from 'react'
export function AdminDashboard() {
const [stats, setStats] = useState<SystemStats | null>(null)
useEffect(() => {
const loadStats = async () => {
const { data } = await apiRequest<SystemStats>('/stats')
setStats(data)
}
loadStats()
const interval = setInterval(loadStats, 10000) // Обновляем каждые 10 сек
return () => clearInterval(interval)
}, [])
if (!stats) return <div>Загрузка...</div>
return (
<div className="admin-dashboard">
<h2>Панель администратора</h2>
<div className="dashboard-grid">
<div className="dashboard-card">
<h3>Пользователи</h3>
<div className="big-number">{stats.users}</div>
</div>
<div className="dashboard-card">
<h3>Задания</h3>
<div className="big-number">{stats.tasks}</div>
</div>
<div className="dashboard-card">
<h3>Цепочки</h3>
<div className="big-number">{stats.chains}</div>
</div>
<div className="dashboard-card">
<h3>Всего проверок</h3>
<div className="big-number">{stats.submissions.total}</div>
</div>
</div>
<div className="submissions-chart">
<h3>Статистика проверок</h3>
<div className="chart-bars">
<div className="bar accepted" style={{ height: `${(stats.submissions.accepted / stats.submissions.total) * 100}%` }}>
<span>Принято: {stats.submissions.accepted}</span>
</div>
<div className="bar rejected" style={{ height: `${(stats.submissions.rejected / stats.submissions.total) * 100}%` }}>
<span>Отклонено: {stats.submissions.rejected}</span>
</div>
<div className="bar pending" style={{ height: `${(stats.submissions.pending / stats.submissions.total) * 100}%` }}>
<span>Ожидают: {stats.submissions.pending}</span>
</div>
</div>
</div>
<div className="queue-status">
<h3>Статус очереди</h3>
<div className="queue-info">
<p>🔄 В обработке: {stats.queue.currentlyProcessing} / {stats.queue.maxConcurrency}</p>
<p> В ожидании: {stats.queue.waiting}</p>
<p>📊 Всего в очереди: {stats.queue.queueLength}</p>
</div>
</div>
<div className="avg-time">
<h3>Среднее время проверки</h3>
<div className="big-number">{Math.round(stats.averageCheckTimeMs / 1000)}с</div>
</div>
</div>
)
}
```
---
## State Management
### React Context пример
```typescript
// ChallengeContext.tsx
import { createContext, useContext, useState, useEffect } from 'react'
interface ChallengeContextType {
userId: string | null
nickname: string | null
login: (nickname: string) => Promise<void>
logout: () => void
stats: UserStats | null
refreshStats: () => Promise<void>
}
const ChallengeContext = createContext<ChallengeContextType | undefined>(undefined)
export function ChallengeProvider({ children }: { children: React.ReactNode }) {
const [userId, setUserId] = useState<string | null>(
localStorage.getItem('challengeUserId')
)
const [nickname, setNickname] = useState<string | null>(
localStorage.getItem('challengeNickname')
)
const [stats, setStats] = useState<UserStats | null>(null)
const login = async (nickname: string) => {
const userId = await authenticateUser(nickname)
setUserId(userId)
setNickname(nickname)
localStorage.setItem('challengeNickname', nickname)
}
const logout = () => {
setUserId(null)
setNickname(null)
setStats(null)
localStorage.removeItem('challengeUserId')
localStorage.removeItem('challengeNickname')
}
const refreshStats = async () => {
if (userId) {
const userStats = await getUserStats(userId)
setStats(userStats)
}
}
useEffect(() => {
if (userId) {
refreshStats()
}
}, [userId])
return (
<ChallengeContext.Provider value={{
userId,
nickname,
login,
logout,
stats,
refreshStats
}}>
{children}
</ChallengeContext.Provider>
)
}
export function useChallenge() {
const context = useContext(ChallengeContext)
if (!context) {
throw new Error('useChallenge must be used within ChallengeProvider')
}
return context
}
```
---
## Best Practices
### 1. Polling оптимизация
```typescript
// Используйте exponential backoff для polling
class PollingManager {
private intervalId: number | null = null
private currentDelay = 2000 // Начинаем с 2 секунд
private maxDelay = 10000 // Максимум 10 секунд
start(callback: () => Promise<boolean>) {
const poll = async () => {
const shouldContinue = await callback()
if (shouldContinue) {
// Увеличиваем delay
this.currentDelay = Math.min(this.currentDelay * 1.5, this.maxDelay)
this.intervalId = window.setTimeout(poll, this.currentDelay)
}
}
poll()
}
stop() {
if (this.intervalId) {
clearTimeout(this.intervalId)
this.intervalId = null
}
}
}
```
### 2. Обработка ошибок
```typescript
// Создайте централизованный обработчик ошибок
class ChallengeAPIError extends Error {
constructor(
public message: string,
public statusCode?: number,
public details?: any
) {
super(message)
}
}
async function handleAPIError(response: Response) {
const data = await response.json()
if (data.error) {
throw new ChallengeAPIError(
data.error.message || 'Unknown error',
response.status,
data.error
)
}
return data
}
```
### 3. Кеширование
```typescript
// Кешируйте редко меняющиеся данные
class ChallengeCache {
private cache = new Map<string, { data: any; expires: number }>()
set(key: string, data: any, ttl: number = 60000) {
this.cache.set(key, {
data,
expires: Date.now() + ttl
})
}
get(key: string): any | null {
const cached = this.cache.get(key)
if (!cached) return null
if (Date.now() > cached.expires) {
this.cache.delete(key)
return null
}
return cached.data
}
}
const cache = new ChallengeCache()
async function getChainsWithCache(): Promise<ChallengeChain[]> {
const cached = cache.get('chains')
if (cached) return cached
const chains = await getChains()
cache.set('chains', chains, 5 * 60 * 1000) // 5 минут
return chains
}
```
### 4. Оптимистичные обновления
```typescript
// Показывайте изменения сразу, до ответа от сервера
async function optimisticSubmit(
task: ChallengeTask,
result: string,
updateUI: (status: 'submitting' | 'waiting' | 'checking') => void
) {
updateUI('submitting')
try {
const { queueId } = await submitSolution(userId, task.id, result)
updateUI('waiting')
const submission = await pollCheckStatus(queueId, (status) => {
if (status.status === 'in_progress') {
updateUI('checking')
}
})
return submission
} catch (err) {
// Откатываем UI если ошибка
throw err
}
}
```
### 5. Accessibility
```typescript
// Добавляйте ARIA атрибуты
<div
role="alert"
aria-live="polite"
aria-atomic="true"
>
{status === 'waiting' && 'Ожидание в очереди...'}
{status === 'in_progress' && 'Проверка решения...'}
</div>
// Используйте semantic HTML
<button
aria-label="Отправить решение на проверку"
disabled={submitting}
aria-busy={submitting}
>
Отправить
</button>
```
### 6. Offline support
```typescript
// Сохраняйте незавершенные решения локально
function saveDraft(taskId: string, result: string) {
localStorage.setItem(`draft_${taskId}`, result)
}
function loadDraft(taskId: string): string | null {
return localStorage.getItem(`draft_${taskId}`)
}
function clearDraft(taskId: string) {
localStorage.removeItem(`draft_${taskId}`)
}
```
---
## Примеры интеграции
### React + TypeScript
Полный пример приложения доступен в репозитории: `/examples/react-challenge-app`
### Vue.js
```typescript
// composition API
import { ref, onMounted } from 'vue'
export function useChallengeSubmission(taskId: string) {
const result = ref('')
const submitting = ref(false)
const checkStatus = ref<QueueStatus | null>(null)
const submit = async () => {
submitting.value = true
try {
const { queueId } = await submitSolution(userId, taskId, result.value)
await pollCheckStatus(queueId, (status) => {
checkStatus.value = status
})
} finally {
submitting.value = false
}
}
return {
result,
submitting,
checkStatus,
submit
}
}
```
---
## Заключение
Этот сервис предоставляет гибкий API для создания различных пользовательских интерфейсов. Основные принципы:
1. **Polling для отслеживания** - используйте разумные интервалы
2. **Оптимистичные обновления** - для лучшего UX
3. **Кеширование** - для производительности
4. **Обработка ошибок** - для надежности
5. **Accessibility** - для всех пользователей
Используйте предоставленные компоненты как основу и адаптируйте под ваш дизайн-систему!