brief-rags-bench/REFACTORING_PLAN.md

23 KiB
Raw Permalink Blame History

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

<!-- Заменить обычные скрипты на модули -->
<script type="module" src="js/main.js"></script>

1.2. Создать структуру папок

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)

Экспорт:

export {
  generateUUID,
  formatTime,
  formatTimestamp,
  isTableText,
  parseTextTable,
  escapeHtml
}

2.2. utils/file.utils.js

Функции (строки 262-273, 984-1132):

  • downloadJSON(data, filename)
  • loadFileAsJSON(file) - новая функция для загрузки
  • loadFileAsText(file) - новая функция

Экспорт:

export {
  downloadJSON,
  loadFileAsJSON,
  loadFileAsText
}

2.3. utils/validation.utils.js

Функции (строки 823-860):

  • validateJSON(jsonString)
  • validateLoginFormat(login)

Экспорт:

export {
  validateJSON,
  validateLoginFormat
}

2.4. utils/dom.utils.js

Функции:

  • showElement(id)
  • hideElement(id)
  • toggleElement(id)
  • setElementText(id, text)
  • showToast(message, type) (строка 277-281)

Экспорт:

export {
  showElement,
  hideElement,
  toggleElement,
  setElementText,
  showToast
}

Этап 3: State Management (день 2)

Цель: Централизовать управление состоянием

3.1. state/appState.js

Содержимое (строки 10-42):

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)

Экспорт:

export {
  saveEnvironmentData,
  loadEnvironmentData,
  clearEnvironmentData
}

3.3. data/defaults.js

Перенести из settings.js (строки 1-70):

export const defaultSettings = {
  activeEnvironment: 'ift',
  environments: { /* ... */ }
}

Этап 4: Services (день 3)

Цель: Бизнес-логика и API взаимодействие

4.1. services/api-client.js

Действие: Переместить существующий api-client.js в папку services/

Обновить:

class BriefBenchAPI {
  // ... существующий код
}

export default new BriefBenchAPI()

4.2. services/auth.service.js

Функции (строки 60-140):

  • checkAuth()
  • login(loginString)
  • logout()
  • isAuthenticated()

Экспорт:

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()

Экспорт:

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()

Экспорт:

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()

Экспорт:

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()

Экспорт:

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()

Экспорт:

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()

Экспорт:

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)

Экспорт:

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)

Экспорт:

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()

Экспорт:

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

// Импорты
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

// 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 во время миграции:

<!-- Старый код (работает) -->
<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 рабочих дня)

Пример использования после рефакторинга

// Было (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