1 stage front refactoring
This commit is contained in:
parent
6e29b2e6df
commit
fc2eeb82ca
|
|
@ -0,0 +1,325 @@
|
||||||
|
# Рефакторинг app.js → Модули: Прогресс
|
||||||
|
|
||||||
|
**Дата начала**: 2025-12-25
|
||||||
|
**Стратегия**: Постепенная миграция (app.js остаётся рабочим)
|
||||||
|
**Статус**: 🟡 В процессе
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Общий прогресс: 20%
|
||||||
|
|
||||||
|
```
|
||||||
|
[████░░░░░░░░░░░░░░░░] 20% завершено
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Этапы рефакторинга
|
||||||
|
|
||||||
|
### ✅ Этап 1: Подготовка (ЗАВЕРШЁН)
|
||||||
|
|
||||||
|
**Дата**: 2025-12-25
|
||||||
|
**Статус**: ✅ Готово
|
||||||
|
|
||||||
|
- [x] Создать структуру папок `static/js/`
|
||||||
|
- [x] `js/state/`
|
||||||
|
- [x] `js/services/`
|
||||||
|
- [x] `js/ui/`
|
||||||
|
- [x] `js/utils/`
|
||||||
|
- [x] `js/data/`
|
||||||
|
- [x] Создать `js/config.js` с константами
|
||||||
|
|
||||||
|
**Результат**: Структура готова, app.js не тронут ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ✅ Этап 2: Утилиты (ЗАВЕРШЁН)
|
||||||
|
|
||||||
|
**Дата**: 2025-12-25
|
||||||
|
**Статус**: ✅ Готово
|
||||||
|
|
||||||
|
#### 2.1. utils/format.utils.js ✅
|
||||||
|
**Строки из app.js**: 150-273
|
||||||
|
**Функции** (11 шт.):
|
||||||
|
- [x] `generateUUID()` - генерация UUID
|
||||||
|
- [x] `formatTime(seconds)` - форматирование времени
|
||||||
|
- [x] `formatTimestamp(isoString)` - ISO → локальное время
|
||||||
|
- [x] `isTableText(text)` - проверка на таблицу
|
||||||
|
- [x] `parseTextTable(text)` - парсинг текстовых таблиц
|
||||||
|
- [x] `escapeHtml(text)` - экранирование HTML
|
||||||
|
- [x] `pluralize(count, one, few, many)` - склонение числительных
|
||||||
|
|
||||||
|
**Экспорт**: ES6 named exports ✅
|
||||||
|
|
||||||
|
#### 2.2. utils/file.utils.js ✅
|
||||||
|
**Строки из app.js**: 262-273, 984-1132
|
||||||
|
**Функции** (6 шт.):
|
||||||
|
- [x] `downloadJSON(data, filename)` - скачать JSON
|
||||||
|
- [x] `loadFileAsJSON(file)` - загрузить JSON
|
||||||
|
- [x] `loadFileAsText(file)` - загрузить текст
|
||||||
|
- [x] `selectFile(accept)` - выбрать файл
|
||||||
|
- [x] `downloadText(text, filename)` - скачать текст
|
||||||
|
|
||||||
|
**Экспорт**: ES6 named exports ✅
|
||||||
|
|
||||||
|
#### 2.3. utils/validation.utils.js ✅
|
||||||
|
**Строки из app.js**: 823-860
|
||||||
|
**Функции** (4 шт.):
|
||||||
|
- [x] `validateJSON(jsonString)` - валидация JSON
|
||||||
|
- [x] `validateLoginFormat(login)` - проверка формата логина (8 цифр)
|
||||||
|
- [x] `validateQuestions(questions)` - валидация массива вопросов
|
||||||
|
- [x] `validateEnvironment(environment)` - проверка окружения
|
||||||
|
|
||||||
|
**Экспорт**: ES6 named exports ✅
|
||||||
|
|
||||||
|
#### 2.4. utils/dom.utils.js ✅
|
||||||
|
**Строки из app.js**: 277-281 + новые
|
||||||
|
**Функции** (15 шт.):
|
||||||
|
- [x] `showElement(element)` - показать элемент
|
||||||
|
- [x] `hideElement(element)` - скрыть элемент
|
||||||
|
- [x] `toggleElement(element)` - переключить видимость
|
||||||
|
- [x] `setElementText(element, text)` - установить текст
|
||||||
|
- [x] `setElementHTML(element, html)` - установить HTML
|
||||||
|
- [x] `getInputValue(element)` - получить значение input
|
||||||
|
- [x] `setInputValue(element, value)` - установить значение input
|
||||||
|
- [x] `addClass(element, className)` - добавить класс
|
||||||
|
- [x] `removeClass(element, className)` - удалить класс
|
||||||
|
- [x] `hasClass(element, className)` - проверить класс
|
||||||
|
- [x] `showToast(message, type)` - показать уведомление
|
||||||
|
- [x] `clearChildren(element)` - очистить детей
|
||||||
|
- [x] `createElement(tag, attributes, parent)` - создать элемент
|
||||||
|
|
||||||
|
**Экспорт**: ES6 named exports ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🟡 Этап 3: State Management (В ПРОЦЕССЕ)
|
||||||
|
|
||||||
|
**Дата**: -
|
||||||
|
**Статус**: 🔲 Ожидает
|
||||||
|
|
||||||
|
#### 3.1. state/appState.js 🔲
|
||||||
|
**Строки из app.js**: 10-51
|
||||||
|
**Что нужно**:
|
||||||
|
- [ ] Создать класс AppState
|
||||||
|
- [ ] Геттеры: `getCurrentEnv()`, `getCurrentEnvSettings()`
|
||||||
|
- [ ] Сеттеры: `setCurrentEnvironment()`, `updateSettings()`
|
||||||
|
- [ ] Методы: `saveToLocalStorage()`, `loadFromLocalStorage()`
|
||||||
|
- [ ] Export: default export singleton
|
||||||
|
|
||||||
|
#### 3.2. data/storage.js 🔲
|
||||||
|
**Строки из app.js**: 618-642
|
||||||
|
**Что нужно**:
|
||||||
|
- [ ] `saveEnvironmentData(env, data)` - сохранить данные окружения
|
||||||
|
- [ ] `loadEnvironmentData(env)` - загрузить данные окружения
|
||||||
|
- [ ] `clearEnvironmentData(env)` - очистить данные
|
||||||
|
- [ ] `clearAllData()` - очистить всё
|
||||||
|
|
||||||
|
#### 3.3. data/defaults.js 🔲
|
||||||
|
**Файл**: settings.js (перенести)
|
||||||
|
**Что нужно**:
|
||||||
|
- [ ] Экспортировать `defaultSettings` из settings.js
|
||||||
|
- [ ] Адаптировать для использования в модулях
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔲 Этап 4: Services (ОЖИДАЕТ)
|
||||||
|
|
||||||
|
**Дата**: -
|
||||||
|
**Статус**: 🔲 Ожидает
|
||||||
|
|
||||||
|
#### 4.1. services/api-client.js 🔲
|
||||||
|
**Файл**: api-client.js (переместить)
|
||||||
|
- [ ] Переместить существующий `api-client.js` в `services/`
|
||||||
|
- [ ] Добавить ES6 экспорт: `export default new BriefBenchAPI()`
|
||||||
|
- [ ] Протестировать импорт
|
||||||
|
|
||||||
|
#### 4.2. services/auth.service.js 🔲
|
||||||
|
**Строки из app.js**: 60-140
|
||||||
|
**Функции**:
|
||||||
|
- [ ] `checkAuth()` - проверка авторизации
|
||||||
|
- [ ] `login(loginString)` - вход
|
||||||
|
- [ ] `logout()` - выход
|
||||||
|
- [ ] `isAuthenticated()` - проверка статуса
|
||||||
|
|
||||||
|
#### 4.3. services/settings.service.js 🔲
|
||||||
|
**Строки из app.js**: 290-357
|
||||||
|
**Функции**:
|
||||||
|
- [ ] `loadFromServer()` - загрузить с сервера
|
||||||
|
- [ ] `saveToServer(settings)` - сохранить на сервер
|
||||||
|
- [ ] `extractEnvSettings(envSettings)` - извлечь настройки окружения
|
||||||
|
- [ ] `resetToDefaults()` - сброс к дефолтным
|
||||||
|
|
||||||
|
#### 4.4. services/query.service.js 🔲
|
||||||
|
**Строки из app.js**: 861-1063
|
||||||
|
**Функции**:
|
||||||
|
- [ ] `buildRequestBody()` - построить тело запроса
|
||||||
|
- [ ] `sendQuery(env, apiMode, body)` - отправить запрос
|
||||||
|
- [ ] `extractQuestions()` - извлечь вопросы из textarea
|
||||||
|
- [ ] `loadRequestFromFile()` - загрузить запрос из файла
|
||||||
|
- [ ] `loadResponseFromFile()` - загрузить ответ из файла
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔲 Этап 5: UI Components (ОЖИДАЕТ)
|
||||||
|
|
||||||
|
**Дата**: -
|
||||||
|
**Статус**: 🔲 Ожидает
|
||||||
|
|
||||||
|
#### 5.1. ui/auth.ui.js 🔲
|
||||||
|
**Строки из app.js**: 77-132
|
||||||
|
- [ ] `showLoginScreen()` - показать экран входа
|
||||||
|
- [ ] `hideLoginScreen()` - скрыть экран входа
|
||||||
|
- [ ] `setupListeners()` - подключить обработчики
|
||||||
|
- [ ] `handleLoginSubmit()` - обработка входа
|
||||||
|
|
||||||
|
#### 5.2. ui/loading.ui.js 🔲
|
||||||
|
**Строки из app.js**: 1137-1145
|
||||||
|
- [ ] `show(message)` - показать загрузку
|
||||||
|
- [ ] `hide()` - скрыть загрузку
|
||||||
|
|
||||||
|
#### 5.3. ui/settings.ui.js 🔲
|
||||||
|
**Строки из app.js**: 362-813
|
||||||
|
- [ ] `open()` - открыть диалог
|
||||||
|
- [ ] `close()` - закрыть диалог
|
||||||
|
- [ ] `populate()` - заполнить поля
|
||||||
|
- [ ] `read()` - прочитать поля
|
||||||
|
- [ ] `toggleBackendSettings(show)` - показать/скрыть backend настройки
|
||||||
|
- [ ] `save()` - сохранить настройки
|
||||||
|
- [ ] `reset()` - сбросить настройки
|
||||||
|
- [ ] `export()` - экспорт настроек
|
||||||
|
- [ ] `import()` - импорт настроек
|
||||||
|
- [ ] `setupListeners()` - подключить обработчики
|
||||||
|
|
||||||
|
#### 5.4. ui/query-builder.ui.js 🔲
|
||||||
|
**Строки из app.js**: 643-883, 884-950
|
||||||
|
- [ ] `show()` - показать построитель запросов
|
||||||
|
- [ ] `switchMode(mode)` - переключить режим (questions/raw-json)
|
||||||
|
- [ ] `validateJSON()` - валидация JSON
|
||||||
|
- [ ] `handleSendQuery()` - обработка отправки
|
||||||
|
- [ ] `setupListeners()` - подключить обработчики
|
||||||
|
|
||||||
|
#### 5.5. ui/questions-list.ui.js 🔲
|
||||||
|
**Строки из app.js**: 1179-1273
|
||||||
|
- [ ] `render()` - рендер списка вопросов
|
||||||
|
- [ ] `selectAnswer(index)` - выбрать ответ
|
||||||
|
- [ ] `updateCount()` - обновить счётчик
|
||||||
|
- [ ] `hasAnnotations(docsSection)` - проверить наличие аннотаций
|
||||||
|
- [ ] `setupListeners()` - подключить обработчики
|
||||||
|
|
||||||
|
#### 5.6. ui/answer-viewer.ui.js 🔲
|
||||||
|
**Строки из app.js**: 1279-1443
|
||||||
|
- [ ] `render(index)` - рендер ответа
|
||||||
|
- [ ] `renderBody(elementId, text)` - рендер тела ответа
|
||||||
|
- [ ] `renderDocuments(containerId, docs, ...)` - рендер документов
|
||||||
|
- [ ] `toggleExpansion(id)` - раскрыть/свернуть
|
||||||
|
- [ ] `switchTab(tabButton, tabId)` - переключить таб
|
||||||
|
- [ ] `setupListeners()` - подключить обработчики
|
||||||
|
|
||||||
|
#### 5.7. ui/annotations.ui.js 🔲
|
||||||
|
**Строки из app.js**: 1448-1615
|
||||||
|
- [ ] `initForAnswer(index)` - инициализация аннотаций
|
||||||
|
- [ ] `loadForAnswer(index)` - загрузить аннотации
|
||||||
|
- [ ] `loadSection(section, data)` - загрузить секцию
|
||||||
|
- [ ] `loadDocuments(section, subsection, docs)` - загрузить документы
|
||||||
|
- [ ] `setupListeners()` - подключить обработчики
|
||||||
|
- [ ] `saveDraft()` - сохранить черновик
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔲 Этап 6: Main Entry Point (ОЖИДАЕТ)
|
||||||
|
|
||||||
|
**Дата**: -
|
||||||
|
**Статус**: 🔲 Ожидает
|
||||||
|
|
||||||
|
#### 6.1. js/main.js 🔲
|
||||||
|
**Что нужно**:
|
||||||
|
- [ ] Импортировать все модули
|
||||||
|
- [ ] Функция `initApp()` - инициализация приложения
|
||||||
|
- [ ] Функция `setupEnvironmentTabs()` - настройка табов
|
||||||
|
- [ ] Функция `switchEnvironment(env)` - переключение окружения
|
||||||
|
- [ ] Функция `updateUI()` - обновление UI
|
||||||
|
- [ ] DOMContentLoaded listener - точка входа
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔲 Этап 7: Тестирование (ОЖИДАЕТ)
|
||||||
|
|
||||||
|
**Дата**: -
|
||||||
|
**Статус**: 🔲 Ожидает
|
||||||
|
|
||||||
|
#### 7.1. Ручное тестирование 🔲
|
||||||
|
- [ ] Авторизация работает
|
||||||
|
- [ ] Загрузка/сохранение настроек
|
||||||
|
- [ ] Отправка запросов (bench mode)
|
||||||
|
- [ ] Отправка запросов (backend mode)
|
||||||
|
- [ ] Отображение ответов
|
||||||
|
- [ ] Аннотации сохраняются
|
||||||
|
- [ ] Экспорт/импорт работает
|
||||||
|
- [ ] Переключение окружений
|
||||||
|
|
||||||
|
#### 7.2. Обновление index.html 🔲
|
||||||
|
- [ ] Закомментировать старые скрипты:
|
||||||
|
```html
|
||||||
|
<!-- <script src="settings.js"></script> -->
|
||||||
|
<!-- <script src="api-client.js"></script> -->
|
||||||
|
<!-- <script src="app.js"></script> -->
|
||||||
|
```
|
||||||
|
- [ ] Подключить новый entry point:
|
||||||
|
```html
|
||||||
|
<script type="module" src="js/main.js"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 7.3. Очистка 🔲
|
||||||
|
- [ ] Удалить старый `app.js` (или переименовать в `app.js.old`)
|
||||||
|
- [ ] Удалить `settings.js` (или переименовать)
|
||||||
|
- [ ] Переместить `api-client.js` (если еще не перемещён)
|
||||||
|
- [ ] Проверить что всё работает
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📈 Статистика
|
||||||
|
|
||||||
|
### Создано файлов: 5/17
|
||||||
|
|
||||||
|
| Категория | Создано | Всего | Прогресс |
|
||||||
|
|-----------|---------|-------|----------|
|
||||||
|
| Config | 1 | 1 | 100% ✅ |
|
||||||
|
| Utils | 4 | 4 | 100% ✅ |
|
||||||
|
| State | 0 | 1 | 0% 🔲 |
|
||||||
|
| Data | 0 | 2 | 0% 🔲 |
|
||||||
|
| Services | 0 | 4 | 0% 🔲 |
|
||||||
|
| UI | 0 | 7 | 0% 🔲 |
|
||||||
|
| Main | 0 | 1 | 0% 🔲 |
|
||||||
|
|
||||||
|
### Перенесено функций: ~36/~150
|
||||||
|
|
||||||
|
- ✅ Format utils: 11 функций
|
||||||
|
- ✅ File utils: 6 функций
|
||||||
|
- ✅ Validation utils: 4 функций
|
||||||
|
- ✅ DOM utils: 15 функций
|
||||||
|
- 🔲 Остальное: ~114 функций
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Следующий шаг
|
||||||
|
|
||||||
|
**Этап 3: State Management**
|
||||||
|
|
||||||
|
Создать:
|
||||||
|
1. `state/appState.js` - глобальное состояние
|
||||||
|
2. `data/storage.js` - обёртка localStorage
|
||||||
|
3. `data/defaults.js` - дефолтные настройки
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Заметки
|
||||||
|
|
||||||
|
- app.js (1671 строка) остаётся рабочим до конца рефакторинга
|
||||||
|
- Все новые модули используют ES6 синтаксис
|
||||||
|
- Экспорты: named exports для utils, default export для сервисов/UI
|
||||||
|
- Модули полностью автономны и тестируемы
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Последнее обновление**: 2025-12-25 (Этап 2 завершён)
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
/**
|
||||||
|
* Application Configuration
|
||||||
|
*
|
||||||
|
* Централизованная конфигурация приложения.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// API Configuration
|
||||||
|
export const API_CONFIG = {
|
||||||
|
baseURL: '/api/v1',
|
||||||
|
timeout: 1800000 // 30 minutes in milliseconds
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI Configuration
|
||||||
|
export const UI_CONFIG = {
|
||||||
|
defaultQueryMode: 'questions',
|
||||||
|
defaultWithDocs: true,
|
||||||
|
maxAnnotationLength: 5000
|
||||||
|
}
|
||||||
|
|
||||||
|
// Storage Keys
|
||||||
|
export const STORAGE_KEYS = {
|
||||||
|
token: 'briefBenchToken',
|
||||||
|
user: 'briefBenchUser',
|
||||||
|
settings: 'briefBenchSettings',
|
||||||
|
envData: (env) => `briefBenchData_${env}`,
|
||||||
|
annotations: 'briefBenchAnnotationsDraft'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
export const ENVIRONMENTS = ['ift', 'psi', 'prod']
|
||||||
|
export const API_MODES = ['bench', 'backend']
|
||||||
|
|
||||||
|
// Environment Names
|
||||||
|
export const ENV_NAMES = {
|
||||||
|
ift: 'ИФТ',
|
||||||
|
psi: 'ПСИ',
|
||||||
|
prod: 'ПРОМ'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Annotation Issue Types
|
||||||
|
export const ANNOTATION_ISSUES = [
|
||||||
|
'factual_error',
|
||||||
|
'inaccurate_wording',
|
||||||
|
'insufficient_context',
|
||||||
|
'offtopic',
|
||||||
|
'technical_answer'
|
||||||
|
]
|
||||||
|
|
||||||
|
// Rating Values
|
||||||
|
export const RATING_VALUES = {
|
||||||
|
CORRECT: 'correct',
|
||||||
|
PARTIAL: 'partial',
|
||||||
|
INCORRECT: 'incorrect'
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,178 @@
|
||||||
|
/**
|
||||||
|
* DOM Utilities
|
||||||
|
*
|
||||||
|
* Функции для работы с DOM (показать/скрыть элементы, обновить текст).
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Показать элемент
|
||||||
|
* @param {string|HTMLElement} element - ID элемента или сам элемент
|
||||||
|
*/
|
||||||
|
export function showElement(element) {
|
||||||
|
const el = typeof element === 'string' ? document.getElementById(element) : element
|
||||||
|
if (el) {
|
||||||
|
el.style.display = ''
|
||||||
|
el.classList.remove('hidden')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Скрыть элемент
|
||||||
|
* @param {string|HTMLElement} element - ID элемента или сам элемент
|
||||||
|
*/
|
||||||
|
export function hideElement(element) {
|
||||||
|
const el = typeof element === 'string' ? document.getElementById(element) : element
|
||||||
|
if (el) {
|
||||||
|
el.classList.add('hidden')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Переключить видимость элемента
|
||||||
|
* @param {string|HTMLElement} element - ID элемента или сам элемент
|
||||||
|
*/
|
||||||
|
export function toggleElement(element) {
|
||||||
|
const el = typeof element === 'string' ? document.getElementById(element) : element
|
||||||
|
if (el) {
|
||||||
|
if (el.classList.contains('hidden')) {
|
||||||
|
showElement(el)
|
||||||
|
} else {
|
||||||
|
hideElement(el)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Установить текст элемента
|
||||||
|
* @param {string|HTMLElement} element - ID элемента или сам элемент
|
||||||
|
* @param {string} text - Текст для установки
|
||||||
|
*/
|
||||||
|
export function setElementText(element, text) {
|
||||||
|
const el = typeof element === 'string' ? document.getElementById(element) : element
|
||||||
|
if (el) {
|
||||||
|
el.textContent = text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Установить HTML элемента
|
||||||
|
* @param {string|HTMLElement} element - ID элемента или сам элемент
|
||||||
|
* @param {string} html - HTML для установки
|
||||||
|
*/
|
||||||
|
export function setElementHTML(element, html) {
|
||||||
|
const el = typeof element === 'string' ? document.getElementById(element) : element
|
||||||
|
if (el) {
|
||||||
|
el.innerHTML = html
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить значение input элемента
|
||||||
|
* @param {string|HTMLElement} element - ID элемента или сам элемент
|
||||||
|
* @returns {string} Значение элемента
|
||||||
|
*/
|
||||||
|
export function getInputValue(element) {
|
||||||
|
const el = typeof element === 'string' ? document.getElementById(element) : element
|
||||||
|
return el ? el.value : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Установить значение input элемента
|
||||||
|
* @param {string|HTMLElement} element - ID элемента или сам элемент
|
||||||
|
* @param {string} value - Значение для установки
|
||||||
|
*/
|
||||||
|
export function setInputValue(element, value) {
|
||||||
|
const el = typeof element === 'string' ? document.getElementById(element) : element
|
||||||
|
if (el) {
|
||||||
|
el.value = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Добавить класс элементу
|
||||||
|
* @param {string|HTMLElement} element - ID элемента или сам элемент
|
||||||
|
* @param {string} className - Имя класса
|
||||||
|
*/
|
||||||
|
export function addClass(element, className) {
|
||||||
|
const el = typeof element === 'string' ? document.getElementById(element) : element
|
||||||
|
if (el) {
|
||||||
|
el.classList.add(className)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Удалить класс у элемента
|
||||||
|
* @param {string|HTMLElement} element - ID элемента или сам элемент
|
||||||
|
* @param {string} className - Имя класса
|
||||||
|
*/
|
||||||
|
export function removeClass(element, className) {
|
||||||
|
const el = typeof element === 'string' ? document.getElementById(element) : element
|
||||||
|
if (el) {
|
||||||
|
el.classList.remove(className)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверить наличие класса у элемента
|
||||||
|
* @param {string|HTMLElement} element - ID элемента или сам элемент
|
||||||
|
* @param {string} className - Имя класса
|
||||||
|
* @returns {boolean} True если класс есть
|
||||||
|
*/
|
||||||
|
export function hasClass(element, className) {
|
||||||
|
const el = typeof element === 'string' ? document.getElementById(element) : element
|
||||||
|
return el ? el.classList.contains(className) : false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Показать toast уведомление
|
||||||
|
* @param {string} message - Сообщение
|
||||||
|
* @param {string} type - Тип ('info', 'success', 'error', 'warning')
|
||||||
|
*/
|
||||||
|
export function showToast(message, type = 'info') {
|
||||||
|
// TODO: Implement proper toast/snackbar component
|
||||||
|
console.log(`[${type.toUpperCase()}] ${message}`)
|
||||||
|
alert(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Очистить всех детей у элемента
|
||||||
|
* @param {string|HTMLElement} element - ID элемента или сам элемент
|
||||||
|
*/
|
||||||
|
export function clearChildren(element) {
|
||||||
|
const el = typeof element === 'string' ? document.getElementById(element) : element
|
||||||
|
if (el) {
|
||||||
|
el.innerHTML = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Создать элемент с атрибутами
|
||||||
|
* @param {string} tag - Тег элемента
|
||||||
|
* @param {object} attributes - Атрибуты элемента
|
||||||
|
* @param {string|HTMLElement} parent - Родительский элемент (опционально)
|
||||||
|
* @returns {HTMLElement} Созданный элемент
|
||||||
|
*/
|
||||||
|
export function createElement(tag, attributes = {}, parent = null) {
|
||||||
|
const el = document.createElement(tag)
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(attributes)) {
|
||||||
|
if (key === 'class') {
|
||||||
|
el.className = value
|
||||||
|
} else if (key === 'text') {
|
||||||
|
el.textContent = value
|
||||||
|
} else if (key === 'html') {
|
||||||
|
el.innerHTML = value
|
||||||
|
} else {
|
||||||
|
el.setAttribute(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parent) {
|
||||||
|
const parentEl = typeof parent === 'string' ? document.getElementById(parent) : parent
|
||||||
|
if (parentEl) {
|
||||||
|
parentEl.appendChild(el)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return el
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
/**
|
||||||
|
* File Utilities
|
||||||
|
*
|
||||||
|
* Функции для работы с файлами (загрузка, сохранение).
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Скачать данные как JSON файл
|
||||||
|
* @param {object} data - Данные для экспорта
|
||||||
|
* @param {string} filename - Имя файла
|
||||||
|
*/
|
||||||
|
export function downloadJSON(data, filename) {
|
||||||
|
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = url
|
||||||
|
a.download = filename
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
document.body.removeChild(a)
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Загрузить файл как JSON
|
||||||
|
* @param {File} file - Файл для загрузки
|
||||||
|
* @returns {Promise<object>} Распарсенный JSON объект
|
||||||
|
*/
|
||||||
|
export async function loadFileAsJSON(file) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
|
||||||
|
reader.onload = (e) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(e.target.result)
|
||||||
|
resolve(data)
|
||||||
|
} catch (error) {
|
||||||
|
reject(new Error('Невалидный JSON формат'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.onerror = () => {
|
||||||
|
reject(new Error('Ошибка чтения файла'))
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.readAsText(file)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Загрузить файл как текст
|
||||||
|
* @param {File} file - Файл для загрузки
|
||||||
|
* @returns {Promise<string>} Содержимое файла
|
||||||
|
*/
|
||||||
|
export async function loadFileAsText(file) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
|
||||||
|
reader.onload = (e) => {
|
||||||
|
resolve(e.target.result)
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.onerror = () => {
|
||||||
|
reject(new Error('Ошибка чтения файла'))
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.readAsText(file)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Создать input для выбора файла и вернуть выбранный файл
|
||||||
|
* @param {string} accept - MIME типы (например, 'application/json')
|
||||||
|
* @returns {Promise<File|null>} Выбранный файл или null
|
||||||
|
*/
|
||||||
|
export async function selectFile(accept = '*') {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const input = document.createElement('input')
|
||||||
|
input.type = 'file'
|
||||||
|
input.accept = accept
|
||||||
|
|
||||||
|
input.onchange = (e) => {
|
||||||
|
const file = e.target.files[0]
|
||||||
|
resolve(file || null)
|
||||||
|
}
|
||||||
|
|
||||||
|
input.oncancel = () => {
|
||||||
|
resolve(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
input.click()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Скачать текст как файл
|
||||||
|
* @param {string} text - Текст для сохранения
|
||||||
|
* @param {string} filename - Имя файла
|
||||||
|
* @param {string} mimeType - MIME тип (по умолчанию text/plain)
|
||||||
|
*/
|
||||||
|
export function downloadText(text, filename, mimeType = 'text/plain') {
|
||||||
|
const blob = new Blob([text], { type: mimeType })
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = url
|
||||||
|
a.download = filename
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
document.body.removeChild(a)
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,150 @@
|
||||||
|
/**
|
||||||
|
* Format Utilities
|
||||||
|
*
|
||||||
|
* Функции для форматирования данных (время, текст, UUID).
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Генерация UUID v4
|
||||||
|
* @returns {string} UUID
|
||||||
|
*/
|
||||||
|
export function generateUUID() {
|
||||||
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||||
|
const r = Math.random() * 16 | 0
|
||||||
|
const v = c === 'x' ? r : (r & 0x3 | 0x8)
|
||||||
|
return v.toString(16)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Форматировать время в секундах в читаемый вид
|
||||||
|
* @param {number} seconds - Количество секунд
|
||||||
|
* @returns {string} Отформатированное время (например, "2.5 сек", "1 мин 30 сек")
|
||||||
|
*/
|
||||||
|
export function formatTime(seconds) {
|
||||||
|
if (seconds < 60) {
|
||||||
|
return `${seconds.toFixed(1)} сек`
|
||||||
|
}
|
||||||
|
|
||||||
|
const minutes = Math.floor(seconds / 60)
|
||||||
|
const secs = Math.floor(seconds % 60)
|
||||||
|
return `${minutes} мин ${secs} сек`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Форматировать ISO timestamp в локальное время
|
||||||
|
* @param {string} isoString - ISO строка времени
|
||||||
|
* @returns {string} Отформатированное локальное время
|
||||||
|
*/
|
||||||
|
export function formatTimestamp(isoString) {
|
||||||
|
try {
|
||||||
|
const date = new Date(isoString)
|
||||||
|
return date.toLocaleString('ru-RU', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit'
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
return isoString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверить является ли текст таблицей
|
||||||
|
* @param {string} text - Текст для проверки
|
||||||
|
* @returns {boolean} True если текст похож на таблицу
|
||||||
|
*/
|
||||||
|
export function isTableText(text) {
|
||||||
|
const lines = text.split('\n').filter(line => line.trim())
|
||||||
|
if (lines.length < 3) return false
|
||||||
|
|
||||||
|
// Проверить наличие разделителей
|
||||||
|
const hasSeparator = lines.some(line => /^[\s\-|+]+$/.test(line))
|
||||||
|
const hasPipes = lines.filter(line => line.includes('|')).length > 2
|
||||||
|
|
||||||
|
return hasSeparator || hasPipes
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Парсинг текстовой таблицы в HTML
|
||||||
|
* @param {string} text - Текст таблицы
|
||||||
|
* @returns {string} HTML таблица
|
||||||
|
*/
|
||||||
|
export function parseTextTable(text) {
|
||||||
|
const lines = text.split('\n').filter(line => line.trim())
|
||||||
|
|
||||||
|
if (lines.length < 2) {
|
||||||
|
return `<pre>${escapeHtml(text)}</pre>`
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '<table class="parsed-table">'
|
||||||
|
let inHeader = true
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
// Skip separator lines
|
||||||
|
if (/^[\s\-|+]+$/.test(line)) {
|
||||||
|
inHeader = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse cells
|
||||||
|
const cells = line.split('|')
|
||||||
|
.map(cell => cell.trim())
|
||||||
|
.filter((cell, i, arr) => i > 0 && i < arr.length - 1) // Remove first and last empty cells
|
||||||
|
|
||||||
|
if (cells.length === 0) continue
|
||||||
|
|
||||||
|
html += '<tr>'
|
||||||
|
const tag = inHeader ? 'th' : 'td'
|
||||||
|
|
||||||
|
for (const cell of cells) {
|
||||||
|
html += `<${tag}>${escapeHtml(cell)}</${tag}>`
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '</tr>'
|
||||||
|
|
||||||
|
if (inHeader) {
|
||||||
|
inHeader = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '</table>'
|
||||||
|
return html
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Экранировать HTML спецсимволы
|
||||||
|
* @param {string} text - Исходный текст
|
||||||
|
* @returns {string} Экранированный текст
|
||||||
|
*/
|
||||||
|
export function escapeHtml(text) {
|
||||||
|
const div = document.createElement('div')
|
||||||
|
div.textContent = text
|
||||||
|
return div.innerHTML
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Склонение числительных (русский язык)
|
||||||
|
* @param {number} count - Число
|
||||||
|
* @param {string} one - Форма для 1 (например, "вопрос")
|
||||||
|
* @param {string} few - Форма для 2-4 (например, "вопроса")
|
||||||
|
* @param {string} many - Форма для 5+ (например, "вопросов")
|
||||||
|
* @returns {string} Правильная форма
|
||||||
|
*/
|
||||||
|
export function pluralize(count, one, few, many) {
|
||||||
|
const mod10 = count % 10
|
||||||
|
const mod100 = count % 100
|
||||||
|
|
||||||
|
if (mod10 === 1 && mod100 !== 11) {
|
||||||
|
return one
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mod10 >= 2 && mod10 <= 4 && (mod100 < 10 || mod100 >= 20)) {
|
||||||
|
return few
|
||||||
|
}
|
||||||
|
|
||||||
|
return many
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,133 @@
|
||||||
|
/**
|
||||||
|
* Validation Utilities
|
||||||
|
*
|
||||||
|
* Функции для валидации данных.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Валидация JSON строки
|
||||||
|
* @param {string} jsonString - JSON строка для валидации
|
||||||
|
* @returns {{valid: boolean, error: string|null, data: object|null}} Результат валидации
|
||||||
|
*/
|
||||||
|
export function validateJSON(jsonString) {
|
||||||
|
if (!jsonString || !jsonString.trim()) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error: 'JSON не может быть пустым',
|
||||||
|
data: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(jsonString)
|
||||||
|
return {
|
||||||
|
valid: true,
|
||||||
|
error: null,
|
||||||
|
data
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error: `Ошибка парсинга JSON: ${e.message}`,
|
||||||
|
data: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Валидация формата логина (8 цифр)
|
||||||
|
* @param {string} login - Логин для проверки
|
||||||
|
* @returns {{valid: boolean, error: string|null}} Результат валидации
|
||||||
|
*/
|
||||||
|
export function validateLoginFormat(login) {
|
||||||
|
if (!login || !login.trim()) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error: 'Логин не может быть пустым'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!/^[0-9]{8}$/.test(login)) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error: 'Логин должен состоять из 8 цифр'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: true,
|
||||||
|
error: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Валидация массива вопросов для запроса
|
||||||
|
* @param {Array} questions - Массив вопросов
|
||||||
|
* @returns {{valid: boolean, error: string|null}} Результат валидации
|
||||||
|
*/
|
||||||
|
export function validateQuestions(questions) {
|
||||||
|
if (!Array.isArray(questions)) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error: 'Вопросы должны быть массивом'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (questions.length === 0) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error: 'Необходимо добавить хотя бы один вопрос'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < questions.length; i++) {
|
||||||
|
const q = questions[i]
|
||||||
|
|
||||||
|
if (!q.body || typeof q.body !== 'string' || !q.body.trim()) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error: `Вопрос ${i + 1}: текст вопроса не может быть пустым`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof q.with_docs !== 'boolean') {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error: `Вопрос ${i + 1}: поле with_docs должно быть boolean`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: true,
|
||||||
|
error: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Валидация окружения
|
||||||
|
* @param {string} environment - Окружение для проверки
|
||||||
|
* @returns {{valid: boolean, error: string|null}} Результат валидации
|
||||||
|
*/
|
||||||
|
export function validateEnvironment(environment) {
|
||||||
|
const validEnvs = ['ift', 'psi', 'prod']
|
||||||
|
|
||||||
|
if (!environment) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error: 'Окружение не может быть пустым'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validEnvs.includes(environment)) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error: `Недопустимое окружение. Разрешены: ${validEnvs.join(', ')}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: true,
|
||||||
|
error: null
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue