brief-rags-bench/REFACTORING_PLAN.md

817 lines
23 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.

# План рефакторинга 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*