This commit is contained in:
itqop 2025-12-25 11:01:44 +03:00
parent da2a67bc91
commit 6e29b2e6df
1 changed files with 816 additions and 0 deletions

816
REFACTORING_PLAN.md Normal file
View File

@ -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
<!-- Заменить обычные скрипты на модули -->
<script type="module" src="js/main.js"></script>
```
#### 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
<!-- Старый код (работает) -->
<script src="settings.js"></script>
<script src="api-client.js"></script>
<script src="app.js"></script>
<!-- Новый код (тестируется) -->
<!-- <script type="module" src="js/main.js"></script> -->
```
### Вариант 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*