diff --git a/REFACTORING_PLAN.md b/REFACTORING_PLAN.md new file mode 100644 index 0000000..40f9662 --- /dev/null +++ b/REFACTORING_PLAN.md @@ -0,0 +1,816 @@ +# План рефакторинга app.js на ES6 модули + +## Текущее состояние + +- **Размер файла**: 1671 строка +- **Проблемы**: + - Монолитная структура затрудняет поддержку + - Все функции в глобальной области видимости + - Сложно тестировать отдельные компоненты + - Повторное использование кода затруднено + +## Целевая архитектура + +``` +static/ +├── index.html +├── styles.css +├── js/ +│ ├── main.js # Точка входа, инициализация +│ ├── config.js # Константы и конфигурация +│ ├── state/ +│ │ └── appState.js # Глобальное состояние приложения +│ ├── services/ +│ │ ├── api-client.js # Существующий API клиент (переместить) +│ │ ├── auth.service.js # Аутентификация +│ │ ├── settings.service.js # Управление настройками +│ │ └── query.service.js # Запросы к RAG +│ ├── ui/ +│ │ ├── auth.ui.js # UI авторизации +│ │ ├── settings.ui.js # Диалог настроек +│ │ ├── query-builder.ui.js # Построитель запросов +│ │ ├── answer-viewer.ui.js # Просмотр ответов +│ │ ├── questions-list.ui.js # Список вопросов +│ │ ├── annotations.ui.js # Аннотации +│ │ └── loading.ui.js # Индикаторы загрузки +│ ├── utils/ +│ │ ├── dom.utils.js # DOM манипуляции +│ │ ├── format.utils.js # Форматирование (время, текст) +│ │ ├── file.utils.js # Работа с файлами +│ │ └── validation.utils.js # Валидация данных +│ └── data/ +│ ├── storage.js # LocalStorage обертка +│ └── defaults.js # Дефолтные настройки (из settings.js) +└── settings.js # Удалить после рефакторинга +``` + +## Этапы рефакторинга + +### Этап 1: Подготовка (день 1) + +**Цель**: Настроить окружение для ES6 модулей + +#### 1.1. Обновить index.html +```html + + +``` + +#### 1.2. Создать структуру папок +```bash +mkdir static/js +mkdir static/js/state +mkdir static/js/services +mkdir static/js/ui +mkdir static/js/utils +mkdir static/js/data +``` + +#### 1.3. Настроить конфигурацию +- Создать `js/config.js` с константами +- Определить экспорты/импорты + +--- + +### Этап 2: Утилиты и вспомогательные функции (день 1-2) + +**Цель**: Вынести независимые функции + +#### 2.1. `utils/format.utils.js` +**Функции** (из app.js строки 150-188): +- `generateUUID()` +- `formatTime(seconds)` +- `formatTimestamp(isoString)` +- `isTableText(text)` +- `parseTextTable(text)` +- `escapeHtml(text)` + +**Экспорт**: +```javascript +export { + generateUUID, + formatTime, + formatTimestamp, + isTableText, + parseTextTable, + escapeHtml +} +``` + +#### 2.2. `utils/file.utils.js` +**Функции** (строки 262-273, 984-1132): +- `downloadJSON(data, filename)` +- `loadFileAsJSON(file)` - новая функция для загрузки +- `loadFileAsText(file)` - новая функция + +**Экспорт**: +```javascript +export { + downloadJSON, + loadFileAsJSON, + loadFileAsText +} +``` + +#### 2.3. `utils/validation.utils.js` +**Функции** (строки 823-860): +- `validateJSON(jsonString)` +- `validateLoginFormat(login)` + +**Экспорт**: +```javascript +export { + validateJSON, + validateLoginFormat +} +``` + +#### 2.4. `utils/dom.utils.js` +**Функции**: +- `showElement(id)` +- `hideElement(id)` +- `toggleElement(id)` +- `setElementText(id, text)` +- `showToast(message, type)` (строка 277-281) + +**Экспорт**: +```javascript +export { + showElement, + hideElement, + toggleElement, + setElementText, + showToast +} +``` + +--- + +### Этап 3: State Management (день 2) + +**Цель**: Централизовать управление состоянием + +#### 3.1. `state/appState.js` +**Содержимое** (строки 10-42): +```javascript +class AppState { + constructor() { + this.settings = { /* ... */ } + this.currentEnvironment = 'ift' + this.environments = { + ift: { /* ... */ }, + psi: { /* ... */ }, + prod: { /* ... */ } + } + } + + // Геттеры + getCurrentEnv() { /* ... */ } + getCurrentEnvSettings() { /* ... */ } + + // Сеттеры + setCurrentEnvironment(env) { /* ... */ } + updateSettings(settings) { /* ... */ } + + // Persistence + saveToLocalStorage() { /* ... */ } + loadFromLocalStorage() { /* ... */ } +} + +export default new AppState() +``` + +#### 3.2. `data/storage.js` +**Функции**: +- `saveEnvironmentData(env, data)` +- `loadEnvironmentData(env)` +- `clearEnvironmentData(env)` + +**Экспорт**: +```javascript +export { + saveEnvironmentData, + loadEnvironmentData, + clearEnvironmentData +} +``` + +#### 3.3. `data/defaults.js` +**Перенести из settings.js** (строки 1-70): +```javascript +export const defaultSettings = { + activeEnvironment: 'ift', + environments: { /* ... */ } +} +``` + +--- + +### Этап 4: Services (день 3) + +**Цель**: Бизнес-логика и API взаимодействие + +#### 4.1. `services/api-client.js` +**Действие**: Переместить существующий `api-client.js` в папку `services/` + +**Обновить**: +```javascript +class BriefBenchAPI { + // ... существующий код +} + +export default new BriefBenchAPI() +``` + +#### 4.2. `services/auth.service.js` +**Функции** (строки 60-140): +- `checkAuth()` +- `login(loginString)` +- `logout()` +- `isAuthenticated()` + +**Экспорт**: +```javascript +import api from './api-client.js' + +export class AuthService { + async checkAuth() { /* ... */ } + async login(loginString) { /* ... */ } + logout() { /* ... */ } + isAuthenticated() { /* ... */ } +} + +export default new AuthService() +``` + +#### 4.3. `services/settings.service.js` +**Функции** (строки 290-357): +- `loadSettingsFromServer()` +- `saveSettingsToServer(settings)` +- `extractEnvironmentSettings(envSettings)` +- `resetToDefaults()` + +**Экспорт**: +```javascript +import api from './api-client.js' +import appState from '../state/appState.js' + +export class SettingsService { + async loadFromServer() { /* ... */ } + async saveToServer(settings) { /* ... */ } + extractEnvSettings(envSettings) { /* ... */ } + resetToDefaults() { /* ... */ } +} + +export default new SettingsService() +``` + +#### 4.4. `services/query.service.js` +**Функции** (строки 861-1063): +- `buildRequestBody()` +- `sendQuery(environment, apiMode, requestBody)` +- `extractQuestions()` +- `loadRequestFromFile()` +- `loadResponseFromFile()` + +**Экспорт**: +```javascript +import api from './api-client.js' +import appState from '../state/appState.js' + +export class QueryService { + buildRequestBody() { /* ... */ } + async sendQuery(env, apiMode, body) { /* ... */ } + extractQuestions() { /* ... */ } + async loadRequestFromFile() { /* ... */ } + async loadResponseFromFile() { /* ... */ } +} + +export default new QueryService() +``` + +--- + +### Этап 5: UI Components (день 4-5) + +**Цель**: Разделить UI логику по компонентам + +#### 5.1. `ui/auth.ui.js` +**Функции** (строки 77-132): +- `showLoginScreen()` +- `hideLoginScreen()` +- `setupLoginListeners()` +- `handleLoginSubmit()` + +**Экспорт**: +```javascript +import authService from '../services/auth.service.js' + +export class AuthUI { + showLoginScreen() { /* ... */ } + hideLoginScreen() { /* ... */ } + setupListeners() { /* ... */ } + async handleLoginSubmit() { /* ... */ } +} + +export default new AuthUI() +``` + +#### 5.2. `ui/loading.ui.js` +**Функции** (строки 1137-1145): +- `showLoading(message)` +- `hideLoading()` + +**Экспорт**: +```javascript +export class LoadingUI { + show(message) { /* ... */ } + hide() { /* ... */ } +} + +export default new LoadingUI() +``` + +#### 5.3. `ui/settings.ui.js` +**Функции** (строки 362-813): +- `openSettingsDialog()` +- `closeSettingsDialog()` +- `populateSettingsDialog()` +- `readSettingsFromDialog()` +- `toggleBackendSettings(show)` +- `saveSettings()` +- `resetSettings()` +- `exportSettings()` +- `importSettings()` + +**Экспорт**: +```javascript +import settingsService from '../services/settings.service.js' +import appState from '../state/appState.js' + +export class SettingsUI { + open() { /* ... */ } + close() { /* ... */ } + populate() { /* ... */ } + read() { /* ... */ } + toggleBackendSettings(show) { /* ... */ } + async save() { /* ... */ } + async reset() { /* ... */ } + export() { /* ... */ } + async import() { /* ... */ } + setupListeners() { /* ... */ } +} + +export default new SettingsUI() +``` + +#### 5.4. `ui/query-builder.ui.js` +**Функции** (строки 643-883): +- `showQueryBuilder()` +- `switchQueryMode(mode)` +- `validateJSON()` +- `setupQueryBuilderListeners()` + +**Экспорт**: +```javascript +import queryService from '../services/query.service.js' + +export class QueryBuilderUI { + show() { /* ... */ } + switchMode(mode) { /* ... */ } + validateJSON() { /* ... */ } + setupListeners() { /* ... */ } + async handleSendQuery() { /* ... */ } +} + +export default new QueryBuilderUI() +``` + +#### 5.5. `ui/questions-list.ui.js` +**Функции** (строки 1179-1273): +- `renderQuestionsList()` +- `selectAnswer(index)` +- `updateQuestionsCount()` +- `hasAnnotationsInDocs(docsSection)` +- `pluralize(count, one, few, many)` + +**Экспорт**: +```javascript +import appState from '../state/appState.js' + +export class QuestionsListUI { + render() { /* ... */ } + selectAnswer(index) { /* ... */ } + updateCount() { /* ... */ } + hasAnnotations(docsSection) { /* ... */ } + setupListeners() { /* ... */ } +} + +export default new QuestionsListUI() +``` + +#### 5.6. `ui/answer-viewer.ui.js` +**Функции** (строки 1279-1443): +- `renderAnswer(index)` +- `renderAnswerBody(elementId, text)` +- `renderDocuments(containerId, docs, ...)` +- `toggleExpansion(id)` +- `switchTab(tabButton, tabId)` + +**Экспорт**: +```javascript +import appState from '../state/appState.js' +import { formatTime, parseTextTable, escapeHtml } from '../utils/format.utils.js' + +export class AnswerViewerUI { + render(index) { /* ... */ } + renderBody(elementId, text) { /* ... */ } + renderDocuments(containerId, docs, ...) { /* ... */ } + toggleExpansion(id) { /* ... */ } + switchTab(tabButton, tabId) { /* ... */ } + setupListeners() { /* ... */ } +} + +export default new AnswerViewerUI() +``` + +#### 5.7. `ui/annotations.ui.js` +**Функции** (строки 1448-1615): +- `initAnnotationForAnswer(index)` +- `loadAnnotationsForAnswer(index)` +- `loadSectionAnnotation(section, data)` +- `loadDocumentAnnotations(section, subsection, docs)` +- `setupAnnotationListeners()` +- `updateCheckboxStyle(checkbox)` +- `saveAnnotationsDraft()` + +**Экспорт**: +```javascript +import appState from '../state/appState.js' +import { saveEnvironmentData } from '../data/storage.js' + +export class AnnotationsUI { + initForAnswer(index) { /* ... */ } + loadForAnswer(index) { /* ... */ } + loadSection(section, data) { /* ... */ } + loadDocuments(section, subsection, docs) { /* ... */ } + setupListeners() { /* ... */ } + saveDraft() { /* ... */ } +} + +export default new AnnotationsUI() +``` + +--- + +### Этап 6: Main Entry Point (день 6) + +**Цель**: Создать точку входа и инициализацию + +#### 6.1. `js/main.js` +```javascript +// Импорты +import appState from './state/appState.js' +import authService from './services/auth.service.js' +import settingsService from './services/settings.service.js' +import authUI from './ui/auth.ui.js' +import settingsUI from './ui/settings.ui.js' +import queryBuilderUI from './ui/query-builder.ui.js' +import questionsListUI from './ui/questions-list.ui.js' +import answerViewerUI from './ui/answer-viewer.ui.js' +import annotationsUI from './ui/annotations.ui.js' + +// Инициализация приложения +async function initApp() { + // Load settings from server + await settingsService.loadFromServer() + appState.setCurrentEnvironment(appState.settings.activeEnvironment || 'ift') + + // Load saved data for each environment + appState.loadFromLocalStorage() + + // Setup all UI listeners + authUI.setupListeners() + settingsUI.setupListeners() + queryBuilderUI.setupListeners() + questionsListUI.setupListeners() + answerViewerUI.setupListeners() + annotationsUI.setupListeners() + + // Setup environment tabs + setupEnvironmentTabs() + + // Render initial state + updateUI() +} + +// Setup environment tabs +function setupEnvironmentTabs() { + const tabs = document.querySelectorAll('.env-tab') + tabs.forEach(tab => { + tab.addEventListener('click', () => { + switchEnvironment(tab.dataset.env) + }) + }) +} + +// Switch environment +function switchEnvironment(env) { + appState.setCurrentEnvironment(env) + updateEnvironmentTabs() + updateUI() +} + +// Update environment tabs +function updateEnvironmentTabs() { + const tabs = document.querySelectorAll('.env-tab') + tabs.forEach(tab => { + if (tab.dataset.env === appState.currentEnvironment) { + tab.classList.add('active') + } else { + tab.classList.remove('active') + } + }) +} + +// Update UI based on current state +function updateUI() { + questionsListUI.render() + + const env = appState.getCurrentEnv() + if (env.currentResponse && env.currentResponse.answers) { + answerViewerUI.render(env.currentAnswerIndex || 0) + } else { + queryBuilderUI.show() + } +} + +// Entry point +document.addEventListener('DOMContentLoaded', async () => { + const isAuthenticated = await authService.checkAuth() + + if (isAuthenticated) { + await initApp() + } +}) +``` + +#### 6.2. `js/config.js` +```javascript +// API Configuration +export const API_CONFIG = { + baseURL: '/api/v1', + timeout: 1800000 // 30 minutes +} + +// 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'] +``` + +--- + +## Этап 7: Тестирование и оптимизация (день 7) + +### 7.1. Ручное тестирование +- ✅ Авторизация работает +- ✅ Загрузка/сохранение настроек +- ✅ Отправка запросов (bench/backend) +- ✅ Отображение ответов +- ✅ Аннотации сохраняются +- ✅ Экспорт/импорт работает +- ✅ Переключение окружений + +### 7.2. Проверка производительности +- Измерить время загрузки +- Проверить размер бандла +- Оптимизировать импорты + +### 7.3. Очистка +- Удалить старый `app.js` +- Удалить `settings.js` +- Обновить `.gitignore` если нужно + +--- + +## Миграционная стратегия + +### Вариант 1: Постепенная миграция (РЕКОМЕНДУЕТСЯ) + +**Подход**: Модули живут параллельно с монолитом + +1. Создать папку `js/` с модулями +2. Оставить `app.js` работающим +3. Постепенно переносить функции +4. Тестировать после каждого этапа +5. Когда все готово - переключиться на `main.js` + +**Преимущества**: +- Можно откатиться в любой момент +- Меньше риска +- Легче тестировать + +**index.html во время миграции**: +```html + + + + + + + +``` + +### Вариант 2: Быстрая миграция + +**Подход**: Переписать всё за раз + +**НЕ РЕКОМЕНДУЕТСЯ** из-за высокого риска багов + +--- + +## Чек-лист миграции + +### Подготовка +- [ ] Создать ветку `feature/modularize-frontend` +- [ ] Сделать backup текущего app.js +- [ ] Создать структуру папок + +### Этап 1: Утилиты +- [ ] Создать `utils/format.utils.js` +- [ ] Создать `utils/file.utils.js` +- [ ] Создать `utils/validation.utils.js` +- [ ] Создать `utils/dom.utils.js` +- [ ] Тесты: проверить что функции работают + +### Этап 2: State +- [ ] Создать `state/appState.js` +- [ ] Создать `data/storage.js` +- [ ] Создать `data/defaults.js` +- [ ] Тесты: проверить геттеры/сеттеры + +### Этап 3: Services +- [ ] Переместить `api-client.js` в `services/` +- [ ] Создать `services/auth.service.js` +- [ ] Создать `services/settings.service.js` +- [ ] Создать `services/query.service.js` +- [ ] Тесты: проверить API вызовы + +### Этап 4: UI Components +- [ ] Создать `ui/auth.ui.js` +- [ ] Создать `ui/loading.ui.js` +- [ ] Создать `ui/settings.ui.js` +- [ ] Создать `ui/query-builder.ui.js` +- [ ] Создать `ui/questions-list.ui.js` +- [ ] Создать `ui/answer-viewer.ui.js` +- [ ] Создать `ui/annotations.ui.js` +- [ ] Тесты: проверить рендеринг + +### Этап 5: Main +- [ ] Создать `js/main.js` +- [ ] Создать `js/config.js` +- [ ] Обновить `index.html` +- [ ] Тесты: полный цикл работы + +### Этап 6: Очистка +- [ ] Удалить старый `app.js` +- [ ] Удалить `settings.js` +- [ ] Обновить документацию +- [ ] Code review + +--- + +## Преимущества после рефакторинга + +### Для разработки +- ✅ **Модульность**: Каждый файл отвечает за свою область +- ✅ **Тестируемость**: Легко покрыть модули unit-тестами +- ✅ **Читаемость**: Проще найти нужную функцию +- ✅ **Переиспользование**: Функции можно использовать в других проектах + +### Для поддержки +- ✅ **Изоляция**: Баг в одном модуле не затронет другие +- ✅ **Масштабируемость**: Легко добавлять новые функции +- ✅ **Документация**: Каждый модуль самодокументируемый +- ✅ **Team work**: Разные разработчики могут работать над разными модулями + +### Для производительности +- ✅ **Tree shaking**: Неиспользуемый код не попадет в бандл +- ✅ **Lazy loading**: Можно подгружать модули по требованию +- ✅ **Кеширование**: Браузер может кешировать отдельные модули + +--- + +## Риски и митигация + +### Риск 1: Поломка существующего функционала +**Митигация**: Постепенная миграция с тестированием после каждого этапа + +### Риск 2: Увеличение времени загрузки (много файлов) +**Митигация**: Использовать bundler (Vite/Webpack) для production + +### Риск 3: Проблемы совместимости браузеров +**Митигация**: ES6 модули поддерживаются всеми современными браузерами + +### Риск 4: Сложность отладки +**Митигация**: Source maps и понятная структура папок + +--- + +## Следующие шаги + +1. **Обсудить план** с командой +2. **Выбрать стратегию миграции** (постепенная/быстрая) +3. **Создать ветку** для рефакторинга +4. **Начать с Этапа 1** (утилиты) +5. **Тестировать** после каждого этапа +6. **Code review** перед мержем + +--- + +## Временные оценки + +| Этап | Описание | Время | +|------|----------|-------| +| Этап 1 | Подготовка | 2 часа | +| Этап 2 | Утилиты | 3 часа | +| Этап 3 | State | 2 часа | +| Этап 4 | Services | 4 часа | +| Этап 5 | UI Components | 8 часов | +| Этап 6 | Main Entry | 2 часа | +| Этап 7 | Тестирование | 4 часа | +| **ИТОГО** | | **~25 часов** (3-4 рабочих дня) | + +--- + +## Пример использования после рефакторинга + +```javascript +// Было (app.js, строка 900): +async function handleSendQuery() { + const envSettings = getCurrentEnvSettings() + const env = getCurrentEnv() + const apiMode = envSettings.apiMode || 'bench' + // ... 50 строк кода +} + +// Стало (js/ui/query-builder.ui.js): +import queryService from '../services/query.service.js' +import appState from '../state/appState.js' +import loadingUI from './loading.ui.js' + +export class QueryBuilderUI { + async handleSendQuery() { + const envSettings = appState.getCurrentEnvSettings() + const apiMode = envSettings.apiMode || 'bench' + + loadingUI.show('Отправка запроса...') + + try { + const result = await queryService.sendQuery( + appState.currentEnvironment, + apiMode, + this.buildRequestBody() + ) + + // Update state + appState.getCurrentEnv().currentResponse = result.response + + // Re-render + this.hide() + answerViewerUI.render(0) + } catch (error) { + showToast(error.message, 'error') + } finally { + loadingUI.hide() + } + } +} +``` + +**Преимущества**: +- Понятно где искать функцию +- Легко тестировать +- Явные зависимости +- Переиспользуемый код + +--- + +*Автор: Claude Sonnet 4.5* +*Дата: 2025-12-25*