Compare commits

..

No commits in common. "9997897411b566efff93f16e88dda3b80dbc3137" and "fc2eeb82ca218b0eb1e72f878a119d10c26b2564" have entirely different histories.

15 changed files with 103 additions and 2763 deletions

View File

@ -6,10 +6,10 @@
---
## 📊 Общий прогресс: 90%
## 📊 Общий прогресс: 20%
```
[██████████████████░░] 90% завершено
[████░░░░░░░░░░░░░░░░] 20% завершено
```
---
@ -93,170 +93,137 @@
---
### ✅ Этап 3: State Management (ЗАВЕРШЁН)
### 🟡 Этап 3: State Management (В ПРОЦЕССЕ)
**Дата**: 2025-12-25
**Статус**: ✅ Готово
**Дата**: -
**Статус**: 🔲 Ожидает
#### 3.1. state/appState.js
#### 3.1. state/appState.js 🔲
**Строки из app.js**: 10-51
**Что нужно**:
- [x] Создать класс AppState
- [x] Геттеры: `getCurrentEnv()`, `getCurrentEnvSettings()`
- [x] Сеттеры: `setCurrentEnvironment()`, `updateSettings()`
- [x] Методы: `saveToLocalStorage()`, `loadFromLocalStorage()`
- [x] Export: default export singleton
- [ ] Создать класс AppState
- [ ] Геттеры: `getCurrentEnv()`, `getCurrentEnvSettings()`
- [ ] Сеттеры: `setCurrentEnvironment()`, `updateSettings()`
- [ ] Методы: `saveToLocalStorage()`, `loadFromLocalStorage()`
- [ ] Export: default export singleton
**Результат**: Singleton класс с полным управлением состоянием ✅
#### 3.2. data/storage.js ✅
#### 3.2. data/storage.js 🔲
**Строки из app.js**: 618-642
**Что нужно**:
- [x] `saveEnvironmentData(env, data)` - сохранить данные окружения
- [x] `loadEnvironmentData(env)` - загрузить данные окружения
- [x] `clearEnvironmentData(env)` - очистить данные
- [x] `clearAllData()` - очистить всё
- [x] Дополнительно: обертки для token, user, settings, annotations
- [ ] `saveEnvironmentData(env, data)` - сохранить данные окружения
- [ ] `loadEnvironmentData(env)` - загрузить данные окружения
- [ ] `clearEnvironmentData(env)` - очистить данные
- [ ] `clearAllData()` - очистить всё
**Результат**: 19 функций для работы с localStorage ✅
#### 3.3. data/defaults.js ✅
#### 3.3. data/defaults.js 🔲
**Файл**: settings.js (перенести)
**Что нужно**:
- [x] Экспортировать `defaultSettings` из settings.js
- [x] Адаптировать для использования в модулях
- [x] Добавить `defaultEnvironmentState`
**Результат**: ES6 экспорт дефолтных настроек ✅
- [ ] Экспортировать `defaultSettings` из settings.js
- [ ] Адаптировать для использования в модулях
---
### ✅ Этап 4: Services (ЗАВЕРШЁН)
### 🔲 Этап 4: Services (ОЖИДАЕТ)
**Дата**: 2025-12-25
**Статус**: ✅ Готово
**Дата**: -
**Статус**: 🔲 Ожидает
#### 4.1. services/api-client.js
#### 4.1. services/api-client.js 🔲
**Файл**: api-client.js (переместить)
- [x] Переместить существующий `api-client.js` в `services/`
- [x] Добавить ES6 экспорт: `export default new BriefBenchAPI()`
- [x] Использовать storage.js для работы с токенами
- [x] Импортировать API_CONFIG из config.js
- [ ] Переместить существующий `api-client.js` в `services/`
- [ ] Добавить ES6 экспорт: `export default new BriefBenchAPI()`
- [ ] Протестировать импорт
**Результат**: ES6 модуль с singleton экспортом ✅
#### 4.2. services/auth.service.js ✅
#### 4.2. services/auth.service.js 🔲
**Строки из app.js**: 60-140
**Функции**:
- [x] `checkAuth()` - проверка авторизации
- [x] `login(loginString)` - вход
- [x] `logout()` - выход
- [x] `isAuthenticated()` - проверка статуса
- [ ] `checkAuth()` - проверка авторизации
- [ ] `login(loginString)` - вход
- [ ] `logout()` - выход
- [ ] `isAuthenticated()` - проверка статуса
**Результат**: 4 функции для авторизации ✅
#### 4.3. services/settings.service.js ✅
#### 4.3. services/settings.service.js 🔲
**Строки из app.js**: 290-357
**Функции**:
- [x] `loadFromServer()` - загрузить с сервера
- [x] `saveToServer(settings)` - сохранить на сервер
- [x] `extractEnvironmentSettings(envSettings)` - извлечь настройки окружения
- [x] `getCurrentEnvironmentSettings()` - получить настройки текущего окружения
- [x] `updateCurrentEnvironmentSettings()` - обновить настройки
- [ ] `loadFromServer()` - загрузить с сервера
- [ ] `saveToServer(settings)` - сохранить на сервер
- [ ] `extractEnvSettings(envSettings)` - извлечь настройки окружения
- [ ] `resetToDefaults()` - сброс к дефолтным
**Результат**: 5 функций для работы с настройками ✅
#### 4.4. services/query.service.js ✅
#### 4.4. services/query.service.js 🔲
**Строки из app.js**: 861-1063
**Функции**:
- [x] `buildRequestBody()` - построить тело запроса
- [x] `sendQuery(env, apiMode, body)` - отправить запрос
- [x] `processQueryResponse()` - обработать ответ
- [x] `extractQuestions()` - извлечь вопросы из textarea
- [x] `loadRequestFromFile()` - загрузить запрос из файла
- [x] `loadResponseFromFile()` - загрузить ответ из файла
**Результат**: 6 функций для работы с запросами ✅
- [ ] `buildRequestBody()` - построить тело запроса
- [ ] `sendQuery(env, apiMode, body)` - отправить запрос
- [ ] `extractQuestions()` - извлечь вопросы из textarea
- [ ] `loadRequestFromFile()` - загрузить запрос из файла
- [ ] `loadResponseFromFile()` - загрузить ответ из файла
---
### ✅ Этап 5: UI Components (ЗАВЕРШЁН)
### 🔲 Этап 5: UI Components (ОЖИДАЕТ)
**Дата**: 2025-12-25
**Статус**: ✅ Готово
**Дата**: -
**Статус**: 🔲 Ожидает
#### 5.1. ui/auth.ui.js
#### 5.1. ui/auth.ui.js 🔲
**Строки из app.js**: 77-132
- [x] `showLoginScreen()` - показать экран входа
- [x] `hideLoginScreen()` - скрыть экран входа
- [x] `handleLogin()` - обработка входа
- [x] `handleLogout()` - обработка выхода
- [x] `setupListeners()` - подключить обработчики
- [ ] `showLoginScreen()` - показать экран входа
- [ ] `hideLoginScreen()` - скрыть экран входа
- [ ] `setupListeners()` - подключить обработчики
- [ ] `handleLoginSubmit()` - обработка входа
**Результат**: 5 функций для UI авторизации ✅
#### 5.2. ui/loading.ui.js ✅
#### 5.2. ui/loading.ui.js 🔲
**Строки из app.js**: 1137-1145
- [x] `show(message)` - показать загрузку
- [x] `hide()` - скрыть загрузку
- [ ] `show(message)` - показать загрузку
- [ ] `hide()` - скрыть загрузку
**Результат**: 2 функции для индикатора загрузки ✅
#### 5.3. ui/settings.ui.js ✅
#### 5.3. ui/settings.ui.js 🔲
**Строки из app.js**: 362-813
- [x] `open()` - открыть диалог
- [x] `close()` - закрыть диалог
- [x] `populate()` - заполнить поля
- [x] `read()` - прочитать поля
- [x] `toggleBackendSettings(show)` - показать/скрыть backend настройки
- [x] `save()` - сохранить настройки
- [x] `reset()` - сбросить настройки
- [x] `exportSettings()` - экспорт настроек
- [x] `importSettings()` - импорт настроек
- [x] `setupListeners()` - подключить обработчики
- [ ] `open()` - открыть диалог
- [ ] `close()` - закрыть диалог
- [ ] `populate()` - заполнить поля
- [ ] `read()` - прочитать поля
- [ ] `toggleBackendSettings(show)` - показать/скрыть backend настройки
- [ ] `save()` - сохранить настройки
- [ ] `reset()` - сбросить настройки
- [ ] `export()` - экспорт настроек
- [ ] `import()` - импорт настроек
- [ ] `setupListeners()` - подключить обработчики
**Результат**: 10 функций для диалога настроек ✅
#### 5.4. ui/query-builder.ui.js ✅
#### 5.4. ui/query-builder.ui.js 🔲
**Строки из app.js**: 643-883, 884-950
- [x] `show()` - показать построитель запросов
- [x] `switchMode(mode)` - переключить режим (questions/raw-json)
- [x] `validateJSONMode()` - валидация JSON
- [x] `handleSendQuery()` - обработка отправки
- [x] `switchTab()` - переключение табов
- [x] `setupListeners()` - подключить обработчики
- [ ] `show()` - показать построитель запросов
- [ ] `switchMode(mode)` - переключить режим (questions/raw-json)
- [ ] `validateJSON()` - валидация JSON
- [ ] `handleSendQuery()` - обработка отправки
- [ ] `setupListeners()` - подключить обработчики
**Результат**: 6 функций для построителя запросов ✅
#### 5.5. ui/questions-list.ui.js ✅
#### 5.5. ui/questions-list.ui.js 🔲
**Строки из app.js**: 1179-1273
- [x] `render()` - рендер списка вопросов
- [x] `selectAnswer(index)` - выбрать ответ
- [x] `hasAnnotationsInDocs(docsSection)` - проверить наличие аннотаций
- [ ] `render()` - рендер списка вопросов
- [ ] `selectAnswer(index)` - выбрать ответ
- [ ] `updateCount()` - обновить счётчик
- [ ] `hasAnnotations(docsSection)` - проверить наличие аннотаций
- [ ] `setupListeners()` - подключить обработчики
**Результат**: 3 функции для списка вопросов ✅
#### 5.6. ui/answer-viewer.ui.js ✅
#### 5.6. ui/answer-viewer.ui.js 🔲
**Строки из app.js**: 1279-1443
- [x] `show()` - показать просмотрщик
- [x] `render(index)` - рендер ответа
- [x] `renderBody(elementId, text)` - рендер тела ответа
- [x] `renderDocuments(containerId, docs, ...)` - рендер документов
- [x] `toggleExpansion(id)` - раскрыть/свернуть
- [ ] `render(index)` - рендер ответа
- [ ] `renderBody(elementId, text)` - рендер тела ответа
- [ ] `renderDocuments(containerId, docs, ...)` - рендер документов
- [ ] `toggleExpansion(id)` - раскрыть/свернуть
- [ ] `switchTab(tabButton, tabId)` - переключить таб
- [ ] `setupListeners()` - подключить обработчики
**Результат**: 5 функций для просмотра ответов ✅
#### 5.7. ui/annotations.ui.js ✅
#### 5.7. ui/annotations.ui.js 🔲
**Строки из app.js**: 1448-1615
- [x] `initForAnswer(index)` - инициализация аннотаций
- [x] `loadForAnswer(index)` - загрузить аннотации
- [x] `loadSection(section, data)` - загрузить секцию
- [x] `loadDocuments(section, subsection, docs)` - загрузить документы
- [x] `setupListeners()` - подключить обработчики
- [x] `saveDraft()` - сохранить черновик
- [x] `updateCheckboxStyle()` - обновить стиль чекбокса
**Результат**: 7 функций для работы с аннотациями ✅
- [ ] `initForAnswer(index)` - инициализация аннотаций
- [ ] `loadForAnswer(index)` - загрузить аннотации
- [ ] `loadSection(section, data)` - загрузить секцию
- [ ] `loadDocuments(section, subsection, docs)` - загрузить документы
- [ ] `setupListeners()` - подключить обработчики
- [ ] `saveDraft()` - сохранить черновик
---
@ -313,38 +280,36 @@
## 📈 Статистика
### Создано файлов: 19/20
### Создано файлов: 5/17
| Категория | Создано | Всего | Прогресс |
|-----------|---------|-------|----------|
| Config | 1 | 1 | 100% ✅ |
| Utils | 4 | 4 | 100% ✅ |
| State | 1 | 1 | 100% ✅ |
| Data | 2 | 2 | 100% ✅ |
| Services | 4 | 4 | 100% ✅ |
| UI | 7 | 7 | 100% ✅ |
| State | 0 | 1 | 0% 🔲 |
| Data | 0 | 2 | 0% 🔲 |
| Services | 0 | 4 | 0% 🔲 |
| UI | 0 | 7 | 0% 🔲 |
| Main | 0 | 1 | 0% 🔲 |
### Перенесено функций: ~108/~150
### Перенесено функций: ~36/~150
- ✅ Format utils: 11 функций
- ✅ File utils: 6 функций
- ✅ Validation utils: 4 функций
- ✅ DOM utils: 15 функций
- ✅ AppState class: ~15 методов
- ✅ Storage utils: 19 функций
- ✅ Services: ~15 функций (auth 4 + settings 5 + query 6)
- ✅ UI Components: ~38 функций (auth 5 + loading 2 + settings 10 + query-builder 6 + questions-list 3 + answer-viewer 5 + annotations 7)
- 🔲 Остальное: ~42 функции (в основном main.js)
- 🔲 Остальное: ~114 функций
---
## 🎯 Следующий шаг
**Этап 6: Main Entry Point**
**Этап 3: State Management**
Создать главную точку входа:
1. `js/main.js` - импортировать все модули, инициализировать приложение
Создать:
1. `state/appState.js` - глобальное состояние
2. `data/storage.js` - обёртка localStorage
3. `data/defaults.js` - дефолтные настройки
---
@ -357,4 +322,4 @@
---
**Последнее обновление**: 2025-12-25 (Этап 5 завершён - 90% готово!)
**Последнее обновление**: 2025-12-25 (Этап 2 завершён)

View File

@ -1,84 +0,0 @@
/**
* Default Settings
*
* Дефолтные настройки приложения.
*/
/**
* Default settings structure for Brief Bench
* User-editable fields only - server configuration managed by FastAPI backend
*/
export const defaultSettings = {
// Active environment
activeEnvironment: 'ift', // 'ift', 'psi', or 'prod'
// Environment-specific settings
environments: {
ift: {
name: 'ИФТ',
apiMode: 'bench', // 'bench' or 'backend'
// Optional headers for RAG backend
bearerToken: '', // Bearer token for authorization (optional)
systemPlatform: '', // System-Platform header (optional)
systemPlatformUser: '', // System-Platform-User header (optional)
// Backend mode settings
platformUserId: '',
platformId: '',
withClassify: false,
resetSessionMode: true // Reset session after each question
},
psi: {
name: 'ПСИ',
apiMode: 'bench', // 'bench' or 'backend'
// Optional headers for RAG backend
bearerToken: '',
systemPlatform: '',
systemPlatformUser: '',
// Backend mode settings
platformUserId: '',
platformId: '',
withClassify: false,
resetSessionMode: true
},
prod: {
name: 'ПРОМ',
apiMode: 'bench', // 'bench' or 'backend'
// Optional headers for RAG backend
bearerToken: '',
systemPlatform: '',
systemPlatformUser: '',
// Backend mode settings
platformUserId: '',
platformId: '',
withClassify: false,
resetSessionMode: true
}
},
// UI settings
theme: 'light',
autoSaveDrafts: true,
requestTimeout: 1800000, // 30 minutes in milliseconds
// Query settings
defaultWithDocs: true,
defaultQueryMode: 'questions' // 'questions' or 'raw-json'
}
/**
* Default environment state structure
*/
export const defaultEnvironmentState = {
currentRequest: null,
currentResponse: null,
currentAnswerIndex: 0,
annotations: {},
requestTimestamp: null,
requestId: null
}

View File

@ -1,225 +0,0 @@
/**
* Storage Service
*
* Обертка над localStorage для работы с данными приложения.
*/
import { STORAGE_KEYS } from '../config.js'
/**
* Save environment data to localStorage
* @param {string} env - Environment name ('ift', 'psi', 'prod')
* @param {object} data - Environment data to save
*/
export function saveEnvironmentData(env, data) {
try {
const key = STORAGE_KEYS.envData(env)
localStorage.setItem(key, JSON.stringify(data))
} catch (e) {
console.error(`Failed to save data for environment ${env}:`, e)
}
}
/**
* Load environment data from localStorage
* @param {string} env - Environment name ('ift', 'psi', 'prod')
* @returns {object|null} Environment data or null if not found
*/
export function loadEnvironmentData(env) {
try {
const key = STORAGE_KEYS.envData(env)
const savedData = localStorage.getItem(key)
if (!savedData) {
return null
}
return JSON.parse(savedData)
} catch (e) {
console.error(`Failed to load data for environment ${env}:`, e)
return null
}
}
/**
* Clear environment data from localStorage
* @param {string} env - Environment name ('ift', 'psi', 'prod')
*/
export function clearEnvironmentData(env) {
try {
const key = STORAGE_KEYS.envData(env)
localStorage.removeItem(key)
} catch (e) {
console.error(`Failed to clear data for environment ${env}:`, e)
}
}
/**
* Clear all environment data
*/
export function clearAllEnvironmentData() {
const environments = ['ift', 'psi', 'prod']
environments.forEach(env => clearEnvironmentData(env))
}
/**
* Save token to localStorage
* @param {string} token - JWT token
*/
export function saveToken(token) {
try {
localStorage.setItem(STORAGE_KEYS.token, token)
} catch (e) {
console.error('Failed to save token:', e)
}
}
/**
* Load token from localStorage
* @returns {string|null} JWT token or null if not found
*/
export function loadToken() {
try {
return localStorage.getItem(STORAGE_KEYS.token)
} catch (e) {
console.error('Failed to load token:', e)
return null
}
}
/**
* Clear token from localStorage
*/
export function clearToken() {
try {
localStorage.removeItem(STORAGE_KEYS.token)
} catch (e) {
console.error('Failed to clear token:', e)
}
}
/**
* Save user info to localStorage
* @param {object} user - User object
*/
export function saveUser(user) {
try {
localStorage.setItem(STORAGE_KEYS.user, JSON.stringify(user))
} catch (e) {
console.error('Failed to save user:', e)
}
}
/**
* Load user info from localStorage
* @returns {object|null} User object or null if not found
*/
export function loadUser() {
try {
const userData = localStorage.getItem(STORAGE_KEYS.user)
return userData ? JSON.parse(userData) : null
} catch (e) {
console.error('Failed to load user:', e)
return null
}
}
/**
* Clear user info from localStorage
*/
export function clearUser() {
try {
localStorage.removeItem(STORAGE_KEYS.user)
} catch (e) {
console.error('Failed to clear user:', e)
}
}
/**
* Save settings to localStorage
* @param {object} settings - Settings object
*/
export function saveSettings(settings) {
try {
localStorage.setItem(STORAGE_KEYS.settings, JSON.stringify(settings))
} catch (e) {
console.error('Failed to save settings:', e)
}
}
/**
* Load settings from localStorage
* @returns {object|null} Settings object or null if not found
*/
export function loadSettings() {
try {
const settingsData = localStorage.getItem(STORAGE_KEYS.settings)
return settingsData ? JSON.parse(settingsData) : null
} catch (e) {
console.error('Failed to load settings:', e)
return null
}
}
/**
* Clear settings from localStorage
*/
export function clearSettings() {
try {
localStorage.removeItem(STORAGE_KEYS.settings)
} catch (e) {
console.error('Failed to clear settings:', e)
}
}
/**
* Save annotation draft to localStorage
* @param {object} draft - Annotation draft object
*/
export function saveAnnotationDraft(draft) {
try {
localStorage.setItem(STORAGE_KEYS.annotations, JSON.stringify(draft))
} catch (e) {
console.error('Failed to save annotation draft:', e)
}
}
/**
* Load annotation draft from localStorage
* @returns {object|null} Annotation draft or null if not found
*/
export function loadAnnotationDraft() {
try {
const draftData = localStorage.getItem(STORAGE_KEYS.annotations)
return draftData ? JSON.parse(draftData) : null
} catch (e) {
console.error('Failed to load annotation draft:', e)
return null
}
}
/**
* Clear annotation draft from localStorage
*/
export function clearAnnotationDraft() {
try {
localStorage.removeItem(STORAGE_KEYS.annotations)
} catch (e) {
console.error('Failed to clear annotation draft:', e)
}
}
/**
* Clear all data from localStorage
*/
export function clearAllData() {
try {
clearToken()
clearUser()
clearSettings()
clearAllEnvironmentData()
clearAnnotationDraft()
} catch (e) {
console.error('Failed to clear all data:', e)
}
}

View File

@ -1,257 +0,0 @@
/**
* Brief Bench API Client
*
* Взаимодействие с FastAPI backend.
*/
import { saveToken, loadToken, clearToken } from '../data/storage.js'
import { API_CONFIG } from '../config.js'
/**
* BriefBenchAPI class - handles all API communication with FastAPI backend
*/
class BriefBenchAPI {
constructor() {
this.baseURL = API_CONFIG.baseURL
}
// ============================================
// Internal Helpers
// ============================================
_getToken() {
return loadToken()
}
_setToken(token) {
saveToken(token)
}
_clearToken() {
clearToken()
}
_getHeaders(includeAuth = true) {
const headers = {
'Content-Type': 'application/json'
}
if (includeAuth) {
const token = this._getToken()
if (token) {
headers['Authorization'] = `Bearer ${token}`
}
}
return headers
}
async _handleResponse(response) {
// Handle 401 Unauthorized
if (response.status === 401) {
this._clearToken()
throw new Error('Сессия истекла. Пожалуйста, войдите снова.')
}
// Handle 502 Bad Gateway (RAG backend error)
if (response.status === 502) {
throw new Error('RAG backend недоступен или вернул ошибку')
}
// Handle other errors
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
throw new Error(errorData.detail || `HTTP ${response.status}: ${response.statusText}`)
}
// Handle 204 No Content
if (response.status === 204) {
return null
}
return await response.json()
}
async _request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`
try {
const response = await fetch(url, options)
return await this._handleResponse(response)
} catch (error) {
console.error(`API request failed: ${endpoint}`, error)
throw error
}
}
// ============================================
// Auth API
// ============================================
/**
* Авторизация с 8-значным логином
* @param {string} login - 8-значный логин
* @returns {Promise<{access_token: string, user: object}>}
*/
async login(login) {
const response = await this._request(`/auth/login?login=${login}`, {
method: 'POST',
headers: this._getHeaders(false)
})
// Сохранить токен
this._setToken(response.access_token)
return response
}
/**
* Выход (очистка токена)
*/
logout() {
this._clearToken()
window.location.reload()
}
/**
* Проверка авторизации
* @returns {boolean}
*/
isAuthenticated() {
return !!this._getToken()
}
// ============================================
// Settings API
// ============================================
/**
* Получить настройки пользователя для всех окружений
* @returns {Promise<{user_id: string, settings: object, updated_at: string}>}
*/
async getSettings() {
return await this._request('/settings', {
method: 'GET',
headers: this._getHeaders()
})
}
/**
* Обновить настройки пользователя
* @param {object} settings - Объект с настройками для окружений
* @returns {Promise<{user_id: string, settings: object, updated_at: string}>}
*/
async updateSettings(settings) {
return await this._request('/settings', {
method: 'PATCH',
headers: this._getHeaders(),
body: JSON.stringify({ settings })
})
}
// ============================================
// Query API
// ============================================
/**
* Отправить batch запрос (Bench mode)
* @param {string} environment - Окружение (ift/psi/prod)
* @param {Array<{body: string, with_docs: boolean}>} questions - Массив вопросов
* @returns {Promise<{request_id: string, timestamp: string, environment: string, response: object}>}
*/
async benchQuery(environment, questions) {
return await this._request('/query/bench', {
method: 'POST',
headers: this._getHeaders(),
body: JSON.stringify({
environment,
questions
})
})
}
/**
* Отправить последовательные запросы (Backend mode)
* @param {string} environment - Окружение (ift/psi/prod)
* @param {Array<{body: string, with_docs: boolean}>} questions - Массив вопросов
* @param {boolean} resetSession - Сбрасывать ли сессию после каждого вопроса
* @returns {Promise<{request_id: string, timestamp: string, environment: string, response: object}>}
*/
async backendQuery(environment, questions, resetSession = true) {
return await this._request('/query/backend', {
method: 'POST',
headers: this._getHeaders(),
body: JSON.stringify({
environment,
questions,
reset_session: resetSession
})
})
}
// ============================================
// Analysis Sessions API
// ============================================
/**
* Сохранить сессию анализа
* @param {object} sessionData - Данные сессии
* @returns {Promise<object>}
*/
async saveSession(sessionData) {
return await this._request('/analysis/sessions', {
method: 'POST',
headers: this._getHeaders(),
body: JSON.stringify(sessionData)
})
}
/**
* Получить список сессий
* @param {string|null} environment - Фильтр по окружению (опционально)
* @param {number} limit - Лимит результатов
* @param {number} offset - Смещение для пагинации
* @returns {Promise<{sessions: Array, total: number}>}
*/
async getSessions(environment = null, limit = 50, offset = 0) {
const params = new URLSearchParams({ limit, offset })
if (environment) {
params.append('environment', environment)
}
return await this._request(`/analysis/sessions?${params}`, {
method: 'GET',
headers: this._getHeaders()
})
}
/**
* Получить конкретную сессию
* @param {string} sessionId - ID сессии
* @returns {Promise<object>}
*/
async getSession(sessionId) {
return await this._request(`/analysis/sessions/${sessionId}`, {
method: 'GET',
headers: this._getHeaders()
})
}
/**
* Удалить сессию
* @param {string} sessionId - ID сессии
* @returns {Promise<null>}
*/
async deleteSession(sessionId) {
return await this._request(`/analysis/sessions/${sessionId}`, {
method: 'DELETE',
headers: this._getHeaders()
})
}
}
// Export singleton instance as default export
export default new BriefBenchAPI()
// Export class for testing purposes
export { BriefBenchAPI }

View File

@ -1,67 +0,0 @@
/**
* Auth Service
*
* Сервис авторизации пользователей.
*/
import api from './api-client.js'
import settingsService from './settings.service.js'
/**
* Проверить авторизацию при загрузке страницы
* @returns {Promise<boolean>} True если авторизован
*/
export async function checkAuth() {
if (!api.isAuthenticated()) {
return false
}
// Попробовать загрузить настройки (валидация токена)
try {
await settingsService.loadFromServer()
return true
} catch (error) {
console.error('Token validation failed:', error)
return false
}
}
/**
* Авторизация пользователя
* @param {string} login - 8-значный логин
* @returns {Promise<object>} User info
*/
export async function login(login) {
// Валидация формата логина
if (!/^[0-9]{8}$/.test(login)) {
throw new Error('Логин должен состоять из 8 цифр')
}
try {
const response = await api.login(login)
console.log('Login successful:', response.user)
// Загрузить настройки с сервера
await settingsService.loadFromServer()
return response.user
} catch (error) {
console.error('Login failed:', error)
throw error
}
}
/**
* Выход из системы
*/
export function logout() {
api.logout()
}
/**
* Проверить авторизован ли пользователь
* @returns {boolean}
*/
export function isAuthenticated() {
return api.isAuthenticated()
}

View File

@ -1,186 +0,0 @@
/**
* Query Service
*
* Сервис отправки запросов к RAG backend.
*/
import api from './api-client.js'
import appState from '../state/appState.js'
import { validateJSON } from '../utils/validation.utils.js'
import { generateUUID } from '../utils/format.utils.js'
import { loadFileAsJSON, loadFileAsText } from '../utils/file.utils.js'
/**
* Построить тело запроса из UI
* @param {string} mode - Режим ('questions' или 'raw-json')
* @param {string} questionsText - Текст вопросов (для режима questions)
* @param {string} jsonText - JSON текст (для режима raw-json)
* @returns {Array<{body: string, with_docs: boolean}>} Массив вопросов
*/
export function buildRequestBody(mode, questionsText, jsonText) {
if (mode === 'questions') {
const questions = questionsText
.split('\n')
.map(line => line.trim())
.filter(line => line.length > 0)
if (questions.length === 0) {
throw new Error('Введите хотя бы один вопрос')
}
const settings = appState.settings || {}
const defaultWithDocs = settings.defaultWithDocs !== undefined
? settings.defaultWithDocs
: true
return questions.map(q => ({
body: q,
with_docs: defaultWithDocs
}))
} else if (mode === 'raw-json') {
const validation = validateJSON(jsonText)
if (!validation.valid) {
throw new Error(validation.error)
}
return validation.data
} else {
throw new Error(`Неизвестный режим: ${mode}`)
}
}
/**
* Отправить запрос к RAG backend
* @param {string} environment - Окружение (ift/psi/prod)
* @param {string} apiMode - Режим API ('bench' или 'backend')
* @param {Array} questions - Массив вопросов
* @param {boolean} resetSession - Сбрасывать ли сессию (только для backend mode)
* @returns {Promise<object>} API response
*/
export async function sendQuery(environment, apiMode, questions, resetSession = true) {
let apiResponse
if (apiMode === 'bench') {
apiResponse = await api.benchQuery(environment, questions)
} else if (apiMode === 'backend') {
apiResponse = await api.backendQuery(environment, questions, resetSession)
} else {
throw new Error(`Неизвестный режим API: ${apiMode}`)
}
// Validate response format
if (!apiResponse.response ||
!apiResponse.response.answers ||
!Array.isArray(apiResponse.response.answers)) {
throw new Error('Некорректный формат ответа: отсутствует поле "answers"')
}
return apiResponse
}
/**
* Обработать результат запроса и обновить AppState
* @param {string} environment - Окружение
* @param {Array} requestBody - Тело запроса
* @param {object} apiResponse - Ответ от API
*/
export function processQueryResponse(environment, requestBody, apiResponse) {
const env = appState.getEnvironment(environment)
// Update environment state
env.currentRequest = requestBody
env.currentResponse = apiResponse.response
env.requestId = apiResponse.request_id
env.requestTimestamp = apiResponse.timestamp
env.currentAnswerIndex = 0
env.annotations = {}
// Save to localStorage
appState.saveEnvironmentToStorage(environment)
return env
}
/**
* Загрузить запрос из файла
* @param {File} file - JSON файл с запросом
* @returns {Promise<Array>} Массив вопросов
*/
export async function loadRequestFromFile(file) {
try {
const data = await loadFileAsJSON(file)
// Validate it's an array
if (!Array.isArray(data)) {
throw new Error('Файл должен содержать JSON массив')
}
return data
} catch (error) {
console.error('Error loading request from file:', error)
throw new Error(`Ошибка загрузки запроса: ${error.message}`)
}
}
/**
* Загрузить ответ из файла
* @param {File} file - JSON файл с ответом
* @param {string} environment - Текущее окружение
* @returns {Promise<object>} Загруженный ответ
*/
export async function loadResponseFromFile(file, environment) {
try {
const data = await loadFileAsJSON(file)
// Validate response format
if (!data.answers || !Array.isArray(data.answers)) {
throw new Error('Файл должен содержать объект с полем "answers" (массив)')
}
const env = appState.getEnvironment(environment)
// Set response
env.currentResponse = data
env.currentAnswerIndex = 0
env.requestTimestamp = new Date().toISOString()
env.requestId = 'loaded-' + generateUUID()
env.annotations = {}
// Try to reconstruct request from questions in response
env.currentRequest = data.answers.map(answer => ({
body: answer.question,
with_docs: true
}))
// Save to localStorage
appState.saveEnvironmentToStorage(environment)
return data
} catch (error) {
console.error('Error loading response from file:', error)
throw new Error(`Ошибка загрузки ответа: ${error.message}`)
}
}
/**
* Извлечь вопросы из textarea
* @param {string} text - Текст из textarea
* @returns {Array<string>} Массив вопросов
*/
export function extractQuestions(text) {
return text
.split('\n')
.map(line => line.trim())
.filter(line => line.length > 0)
}
// Export as default object
export default {
buildRequestBody,
sendQuery,
processQueryResponse,
loadRequestFromFile,
loadResponseFromFile,
extractQuestions
}

View File

@ -1,117 +0,0 @@
/**
* Settings Service
*
* Сервис управления настройками пользователя.
*/
import api from './api-client.js'
import appState from '../state/appState.js'
import { ENV_NAMES } from '../config.js'
/**
* Загрузить настройки с сервера
* @returns {Promise<object>} Загруженные настройки
*/
export async function loadFromServer() {
try {
const response = await api.getSettings()
// Преобразовать в формат AppState.settings
const settings = {
activeEnvironment: appState.getCurrentEnvironment(),
environments: {
ift: {
name: ENV_NAMES.ift,
...response.settings.ift
},
psi: {
name: ENV_NAMES.psi,
...response.settings.psi
},
prod: {
name: ENV_NAMES.prod,
...response.settings.prod
}
},
requestTimeout: 1800000 // 30 минут (фиксировано)
}
// Обновить AppState
appState.setSettings(settings)
console.log('Settings loaded from server:', settings)
return settings
} catch (error) {
console.error('Failed to load settings from server:', error)
throw error
}
}
/**
* Сохранить настройки на сервер
* @param {object} settings - Настройки для сохранения
* @returns {Promise<object>} Сохранённые настройки
*/
export async function saveToServer(settings) {
try {
// Извлечь только поля, которые сервер ожидает
const settingsToSave = {
ift: extractEnvironmentSettings(settings.environments.ift),
psi: extractEnvironmentSettings(settings.environments.psi),
prod: extractEnvironmentSettings(settings.environments.prod)
}
await api.updateSettings(settingsToSave)
appState.setSettings(settings)
console.log('Settings saved to server')
return settings
} catch (error) {
console.error('Failed to save settings to server:', error)
throw error
}
}
/**
* Извлечь только нужные поля для сервера
* @param {object} envSettings - Настройки окружения
* @returns {object} Очищенные настройки для API
*/
export function extractEnvironmentSettings(envSettings) {
return {
apiMode: envSettings.apiMode,
bearerToken: envSettings.bearerToken || null,
systemPlatform: envSettings.systemPlatform || null,
systemPlatformUser: envSettings.systemPlatformUser || null,
platformUserId: envSettings.platformUserId || null,
platformId: envSettings.platformId || null,
withClassify: envSettings.withClassify || false,
resetSessionMode: envSettings.resetSessionMode !== false
}
}
/**
* Получить настройки текущего окружения
* @returns {object} Настройки текущего окружения
*/
export function getCurrentEnvironmentSettings() {
return appState.getCurrentEnvSettings()
}
/**
* Обновить настройки текущего окружения
* @param {object} envSettings - Новые настройки окружения
*/
export function updateCurrentEnvironmentSettings(envSettings) {
const env = appState.getCurrentEnvironment()
appState.updateEnvironmentSettings(env, envSettings)
}
// Export as default object
export default {
loadFromServer,
saveToServer,
extractEnvironmentSettings,
getCurrentEnvironmentSettings,
updateCurrentEnvironmentSettings
}

View File

@ -1,290 +0,0 @@
/**
* Application State
*
* Singleton класс для управления глобальным состоянием приложения.
*/
import { defaultSettings, defaultEnvironmentState } from '../data/defaults.js'
import {
saveEnvironmentData,
loadEnvironmentData,
saveSettings,
loadSettings
} from '../data/storage.js'
/**
* AppState class - manages global application state
* Implements Singleton pattern
*/
class AppState {
constructor() {
if (AppState.instance) {
return AppState.instance
}
// Settings from server
this.settings = null
// Current active environment: 'ift', 'psi', or 'prod'
this.currentEnvironment = 'ift'
// Environment-specific runtime data
this.environments = {
ift: { ...defaultEnvironmentState },
psi: { ...defaultEnvironmentState },
prod: { ...defaultEnvironmentState }
}
AppState.instance = this
}
/**
* Get current environment
* @returns {string} Current environment name
*/
getCurrentEnvironment() {
return this.currentEnvironment
}
/**
* Set current environment
* @param {string} env - Environment name ('ift', 'psi', 'prod')
*/
setCurrentEnvironment(env) {
if (!['ift', 'psi', 'prod'].includes(env)) {
console.error(`Invalid environment: ${env}`)
return
}
this.currentEnvironment = env
}
/**
* Get current environment state
* @returns {object} Current environment state
*/
getCurrentEnv() {
return this.environments[this.currentEnvironment]
}
/**
* Get current environment settings
* @returns {object} Current environment settings
*/
getCurrentEnvSettings() {
if (!this.settings) {
return null
}
return this.settings.environments[this.currentEnvironment]
}
/**
* Get environment state by name
* @param {string} env - Environment name
* @returns {object} Environment state
*/
getEnvironment(env) {
return this.environments[env]
}
/**
* Set settings
* @param {object} settings - Settings object from server
*/
setSettings(settings) {
this.settings = settings
// Update current environment from settings
if (settings.activeEnvironment) {
this.setCurrentEnvironment(settings.activeEnvironment)
}
}
/**
* Update settings
* @param {object} updates - Settings updates
*/
updateSettings(updates) {
if (!this.settings) {
this.settings = { ...defaultSettings }
}
this.settings = {
...this.settings,
...updates
}
// Update current environment if changed
if (updates.activeEnvironment) {
this.setCurrentEnvironment(updates.activeEnvironment)
}
}
/**
* Update environment-specific settings
* @param {string} env - Environment name
* @param {object} envSettings - Environment settings updates
*/
updateEnvironmentSettings(env, envSettings) {
if (!this.settings) {
this.settings = { ...defaultSettings }
}
if (!this.settings.environments) {
this.settings.environments = {}
}
this.settings.environments[env] = {
...this.settings.environments[env],
...envSettings
}
}
/**
* Set current request for environment
* @param {string} env - Environment name
* @param {object} request - Request object
*/
setRequest(env, request) {
this.environments[env].currentRequest = request
}
/**
* Set current response for environment
* @param {string} env - Environment name
* @param {object} response - Response object
*/
setResponse(env, response) {
this.environments[env].currentResponse = response
}
/**
* Set request metadata
* @param {string} env - Environment name
* @param {string} requestId - Request ID
* @param {string} timestamp - Request timestamp
*/
setRequestMetadata(env, requestId, timestamp) {
this.environments[env].requestId = requestId
this.environments[env].requestTimestamp = timestamp
}
/**
* Set current answer index
* @param {string} env - Environment name
* @param {number} index - Answer index
*/
setCurrentAnswerIndex(env, index) {
this.environments[env].currentAnswerIndex = index
}
/**
* Set annotations for environment
* @param {string} env - Environment name
* @param {object} annotations - Annotations object
*/
setAnnotations(env, annotations) {
this.environments[env].annotations = annotations
}
/**
* Get annotations for environment
* @param {string} env - Environment name
* @returns {object} Annotations object
*/
getAnnotations(env) {
return this.environments[env].annotations || {}
}
/**
* Clear environment data
* @param {string} env - Environment name
*/
clearEnvironment(env) {
this.environments[env] = { ...defaultEnvironmentState }
}
/**
* Save environment data to localStorage
* @param {string} env - Environment name
*/
saveEnvironmentToStorage(env) {
const data = this.environments[env]
saveEnvironmentData(env, data)
}
/**
* Load environment data from localStorage
* @param {string} env - Environment name
*/
loadEnvironmentFromStorage(env) {
const data = loadEnvironmentData(env)
if (data) {
this.environments[env] = data
}
}
/**
* Save settings to localStorage
*/
saveSettingsToStorage() {
if (this.settings) {
saveSettings(this.settings)
}
}
/**
* Load settings from localStorage
*/
loadSettingsFromStorage() {
const settings = loadSettings()
if (settings) {
this.setSettings(settings)
}
}
/**
* Load all data from localStorage
*/
loadAllFromStorage() {
this.loadSettingsFromStorage()
const environments = ['ift', 'psi', 'prod']
environments.forEach(env => this.loadEnvironmentFromStorage(env))
}
/**
* Save all data to localStorage
*/
saveAllToStorage() {
this.saveSettingsToStorage()
const environments = ['ift', 'psi', 'prod']
environments.forEach(env => this.saveEnvironmentToStorage(env))
}
/**
* Reset to default state
*/
reset() {
this.settings = null
this.currentEnvironment = 'ift'
this.environments = {
ift: { ...defaultEnvironmentState },
psi: { ...defaultEnvironmentState },
prod: { ...defaultEnvironmentState }
}
}
}
// Create singleton instance
const appState = new AppState()
// Export singleton instance as default export
export default appState
// Export class for testing purposes
export { AppState }

View File

@ -1,244 +0,0 @@
/**
* Annotations UI
*
* UI компонент для работы с аннотациями ответов.
*/
import appState from '../state/appState.js'
import { getInputValue, setInputValue } from '../utils/dom.utils.js'
/**
* Инициализировать аннотацию для ответа
* @param {number} index - Индекс ответа
*/
export function initForAnswer(index) {
const env = appState.getCurrentEnv()
if (!env.annotations[index]) {
env.annotations[index] = {
overall: { rating: '', comment: '' },
body_research: { issues: [], comment: '' },
body_analytical_hub: { issues: [], comment: '' },
docs_from_vectorstore: { research: {}, analytical_hub: {} },
docs_to_llm: { research: {}, analytical_hub: {} }
}
}
}
/**
* Загрузить аннотации для ответа
* @param {number} index - Индекс ответа
*/
export function loadForAnswer(index) {
initForAnswer(index)
const env = appState.getCurrentEnv()
const annotation = env.annotations[index]
// Load overall rating
const ratingSelect = document.getElementById('overall-rating')
const overallComment = document.getElementById('overall-comment')
if (ratingSelect) {
ratingSelect.value = annotation.overall.rating || ''
}
if (overallComment) {
setInputValue(overallComment, annotation.overall.comment || '')
}
// Load body annotations
loadSection('body_research', annotation.body_research)
loadSection('body_analytical_hub', annotation.body_analytical_hub)
// Load document annotations
loadDocuments('docs_from_vectorstore', 'research', annotation.docs_from_vectorstore?.research)
loadDocuments('docs_from_vectorstore', 'analytical_hub', annotation.docs_from_vectorstore?.analytical_hub)
loadDocuments('docs_to_llm', 'research', annotation.docs_to_llm?.research)
loadDocuments('docs_to_llm', 'analytical_hub', annotation.docs_to_llm?.analytical_hub)
// Setup event listeners for current answer
setupListeners()
}
/**
* Загрузить аннотацию секции (body)
* @param {string} section - Название секции
* @param {object} data - Данные аннотации
*/
export function loadSection(section, data) {
// Load checkboxes
document.querySelectorAll(`input[data-section="${section}"]`).forEach(checkbox => {
if (checkbox.type === 'checkbox') {
const issue = checkbox.dataset.issue
checkbox.checked = data.issues.includes(issue)
updateCheckboxStyle(checkbox)
}
})
// Load comment
const textarea = document.querySelector(`textarea[data-section="${section}"]:not([data-doc-index])`)
if (textarea) {
setInputValue(textarea, data.comment || '')
}
}
/**
* Загрузить аннотации документов
* @param {string} section - Секция (docs_from_vectorstore, docs_to_llm)
* @param {string} subsection - Подсекция (research, analytical_hub)
* @param {object} docs - Объект с аннотациями документов
*/
export function loadDocuments(section, subsection, docs) {
if (!docs) return
Object.keys(docs).forEach(docIndex => {
const data = docs[docIndex]
// Load checkboxes
document.querySelectorAll(
`input[data-section="${section}"][data-subsection="${subsection}"][data-doc-index="${docIndex}"]`
).forEach(checkbox => {
if (checkbox.type === 'checkbox') {
const issue = checkbox.dataset.issue
checkbox.checked = data.issues?.includes(issue) || false
updateCheckboxStyle(checkbox)
}
})
// Load comment
const textarea = document.querySelector(
`textarea[data-section="${section}"][data-subsection="${subsection}"][data-doc-index="${docIndex}"]`
)
if (textarea) {
setInputValue(textarea, data.comment || '')
}
})
}
/**
* Настроить обработчики событий для аннотаций
*/
export function setupListeners() {
const env = appState.getCurrentEnv()
const index = env.currentAnswerIndex
// Overall rating
const ratingSelect = document.getElementById('overall-rating')
const overallComment = document.getElementById('overall-comment')
if (ratingSelect) {
ratingSelect.onchange = (e) => {
env.annotations[index].overall.rating = e.target.value
saveDraft()
}
}
if (overallComment) {
overallComment.oninput = (e) => {
env.annotations[index].overall.comment = getInputValue(e.target)
saveDraft()
}
}
// Section checkboxes and textareas
document.querySelectorAll('input.checkbox, textarea').forEach(element => {
const section = element.dataset.section
const subsection = element.dataset.subsection
const docIndex = element.dataset.docIndex
if (!section) return
if (element.type === 'checkbox') {
element.onchange = (e) => {
const issue = e.target.dataset.issue
if (docIndex !== undefined) {
// Document annotation
if (!env.annotations[index][section]) {
env.annotations[index][section] = { research: {}, analytical_hub: {} }
}
if (!env.annotations[index][section][subsection]) {
env.annotations[index][section][subsection] = {}
}
if (!env.annotations[index][section][subsection][docIndex]) {
env.annotations[index][section][subsection][docIndex] = { issues: [], comment: '' }
}
const issues = env.annotations[index][section][subsection][docIndex].issues
if (e.target.checked) {
if (!issues.includes(issue)) issues.push(issue)
} else {
const idx = issues.indexOf(issue)
if (idx > -1) issues.splice(idx, 1)
}
} else {
// Body annotation
const issues = env.annotations[index][section].issues
if (e.target.checked) {
if (!issues.includes(issue)) issues.push(issue)
} else {
const idx = issues.indexOf(issue)
if (idx > -1) issues.splice(idx, 1)
}
}
updateCheckboxStyle(e.target)
saveDraft()
}
} else if (element.tagName === 'TEXTAREA') {
element.oninput = (e) => {
const value = getInputValue(e.target)
if (docIndex !== undefined) {
// Document annotation
if (!env.annotations[index][section][subsection][docIndex]) {
env.annotations[index][section][subsection][docIndex] = { issues: [], comment: '' }
}
env.annotations[index][section][subsection][docIndex].comment = value
} else if (section !== 'overall') {
// Body annotation
env.annotations[index][section].comment = value
}
saveDraft()
}
}
})
}
/**
* Обновить стиль чекбокса (добавить checked класс к label)
* @param {HTMLElement} checkbox - Чекбокс элемент
*/
export function updateCheckboxStyle(checkbox) {
const label = checkbox.closest('.issue-checkbox')
if (label) {
if (checkbox.checked) {
label.classList.add('checked')
} else {
label.classList.remove('checked')
}
}
}
/**
* Сохранить черновик аннотаций в localStorage
*/
export function saveDraft() {
const currentEnv = appState.getCurrentEnvironment()
appState.saveEnvironmentToStorage(currentEnv)
}
// Export as default object
export default {
initForAnswer,
loadForAnswer,
loadSection,
loadDocuments,
setupListeners,
updateCheckboxStyle,
saveDraft
}

View File

@ -1,246 +0,0 @@
/**
* Answer Viewer UI
*
* UI компонент для просмотра ответов.
*/
import appState from '../state/appState.js'
import { escapeHtml, formatTime, formatTimestamp, isTableText, parseTextTable } from '../utils/format.utils.js'
import { setElementText, setElementHTML, addClass, removeClass, hideElement, showElement } from '../utils/dom.utils.js'
/**
* Показать просмотрщик ответов, скрыть построитель запросов
*/
export function show() {
const queryBuilder = document.getElementById('query-builder')
const answerViewer = document.getElementById('answer-viewer')
if (queryBuilder) {
addClass(queryBuilder, 'hidden')
}
if (answerViewer) {
removeClass(answerViewer, 'hidden')
}
}
/**
* Отрендерить ответ по индексу
* @param {number} index - Индекс ответа
* @param {Function} onLoadAnnotations - Callback для загрузки аннотаций
*/
export function render(index, onLoadAnnotations) {
const env = appState.getCurrentEnv()
const answer = env.currentResponse?.answers[index]
if (!answer) {
console.error('Answer not found at index:', index)
return
}
const isBackendMode = answer.backend_mode === true
// Show answer viewer
show()
// Render question header
setElementText('current-question-number', index + 1)
setElementText('current-question-text', answer.question)
// Render metadata
setElementText('processing-time', isBackendMode ? 'N/A' : formatTime(answer.processing_time_sec))
setElementText('request-id', env.requestId || '-')
setElementText('request-timestamp', env.requestTimestamp ? formatTimestamp(env.requestTimestamp) : '-')
// Render answer bodies
renderBody('body-research-text', answer.body_research)
renderBody('body-analytical-text', answer.body_analytical_hub)
// Show/hide documents sections based on mode
const docsSection = document.querySelector('.answer-section:has(#docs-tabs)')
if (docsSection) {
if (isBackendMode) {
hideElement(docsSection)
} else {
showElement(docsSection)
}
}
if (!isBackendMode) {
// Render documents (only in bench mode)
renderDocuments('vectorstore-research-docs', answer.docs_from_vectorstore?.research, 'docs_from_vectorstore', 'research', index)
renderDocuments('vectorstore-analytical-docs', answer.docs_from_vectorstore?.analytical_hub, 'docs_from_vectorstore', 'analytical_hub', index)
renderDocuments('llm-research-docs', answer.docs_to_llm?.research, 'docs_to_llm', 'research', index)
renderDocuments('llm-analytical-docs', answer.docs_to_llm?.analytical_hub, 'docs_to_llm', 'analytical_hub', index)
}
// Load annotations
if (typeof onLoadAnnotations === 'function') {
onLoadAnnotations(index)
}
}
/**
* Отрендерить тело ответа
* @param {string} elementId - ID элемента
* @param {string} text - Текст ответа
*/
export function renderBody(elementId, text) {
const container = document.getElementById(elementId)
if (!container) {
console.warn(`Element ${elementId} not found`)
return
}
if (!text) {
setElementHTML(container, '<p class="color-warning">Нет данных</p>')
return
}
if (isTableText(text)) {
const table = parseTextTable(text)
if (table) {
setElementHTML(container, `<div class="table-container">${table}</div>`)
return
}
}
// Render as plain text with line breaks
const html = `<p>${escapeHtml(text).replace(/\n/g, '<br>')}</p>`
setElementHTML(container, html)
}
/**
* Отрендерить документы
* @param {string} containerId - ID контейнера
* @param {Array} docs - Массив документов
* @param {string} section - Секция (docs_from_vectorstore, docs_to_llm)
* @param {string} subsection - Подсекция (research, analytical_hub)
* @param {number} answerIndex - Индекс ответа
*/
export function renderDocuments(containerId, docs, section, subsection, answerIndex) {
const container = document.getElementById(containerId)
if (!container) {
console.warn(`Container ${containerId} not found`)
return
}
if (!docs || docs.length === 0) {
setElementHTML(container, `
<div class="empty-state">
<div class="empty-state-text">Нет документов</div>
</div>
`)
return
}
const html = docs.map((doc, docIndex) => {
const docId = `doc-${section}-${subsection}-${docIndex}`
let docContent = ''
if (typeof doc === 'string') {
if (isTableText(doc)) {
const table = parseTextTable(doc)
docContent = table || `<pre class="text-table">${escapeHtml(doc)}</pre>`
} else {
docContent = `<p>${escapeHtml(doc).replace(/\n/g, '<br>')}</p>`
}
} else {
docContent = `<pre class="text-table">${escapeHtml(JSON.stringify(doc, null, 2))}</pre>`
}
return `
<div class="expansion-panel" id="${docId}">
<div class="expansion-header" onclick="window.toggleExpansion('${docId}')">
<span class="expansion-header-title">Документ #${docIndex + 1}</span>
<span class="material-icons expansion-icon">expand_more</span>
</div>
<div class="expansion-content">
<div class="expansion-body">
${docContent}
<div class="annotation-section mt-md">
<h6 class="mb-sm">Пометки</h6>
<div class="annotation-issues mb-md">
<label class="issue-checkbox">
<input type="checkbox" class="checkbox"
data-section="${section}"
data-subsection="${subsection}"
data-doc-index="${docIndex}"
data-issue="factual_error">
<span>Факт. ошибка</span>
</label>
<label class="issue-checkbox">
<input type="checkbox" class="checkbox"
data-section="${section}"
data-subsection="${subsection}"
data-doc-index="${docIndex}"
data-issue="inaccurate_wording">
<span>Неточность формулировки</span>
</label>
<label class="issue-checkbox">
<input type="checkbox" class="checkbox"
data-section="${section}"
data-subsection="${subsection}"
data-doc-index="${docIndex}"
data-issue="insufficient_context">
<span>Недостаточно контекста</span>
</label>
<label class="issue-checkbox">
<input type="checkbox" class="checkbox"
data-section="${section}"
data-subsection="${subsection}"
data-doc-index="${docIndex}"
data-issue="offtopic">
<span>Не по теме</span>
</label>
<label class="issue-checkbox">
<input type="checkbox" class="checkbox"
data-section="${section}"
data-subsection="${subsection}"
data-doc-index="${docIndex}"
data-issue="technical_answer">
<span>Технический ответ</span>
</label>
</div>
<div class="form-group">
<label>Комментарий</label>
<textarea class="textarea"
data-section="${section}"
data-subsection="${subsection}"
data-doc-index="${docIndex}"
data-field="comment"
placeholder="Комментарий к документу..."></textarea>
</div>
</div>
</div>
</div>
</div>
`
}).join('')
setElementHTML(container, html)
}
/**
* Переключить раскрытие expansion panel
* @param {string} id - ID панели
*/
export function toggleExpansion(id) {
const panel = document.getElementById(id)
if (panel) {
panel.classList.toggle('expanded')
}
}
// Export as default object
export default {
show,
render,
renderBody,
renderDocuments,
toggleExpansion
}

View File

@ -1,123 +0,0 @@
/**
* Auth UI
*
* UI компонент для экрана авторизации.
*/
import * as authService from '../services/auth.service.js'
import { showElement, hideElement, getInputValue, setInputValue, setElementText } from '../utils/dom.utils.js'
/**
* Показать экран авторизации
*/
export function showLoginScreen() {
const loginScreen = document.getElementById('login-screen')
const app = document.getElementById('app')
if (loginScreen) {
loginScreen.style.display = 'flex'
}
if (app) {
app.style.display = 'none'
}
}
/**
* Скрыть экран авторизации и показать приложение
*/
export function hideLoginScreen() {
const loginScreen = document.getElementById('login-screen')
const app = document.getElementById('app')
if (loginScreen) {
loginScreen.style.display = 'none'
}
if (app) {
app.style.display = 'block'
}
}
/**
* Обработка авторизации
*/
export async function handleLogin() {
const loginInput = document.getElementById('login-input')
const loginError = document.getElementById('login-error')
const loginBtn = document.getElementById('login-submit-btn')
if (!loginInput || !loginError || !loginBtn) {
console.error('Login form elements not found')
return
}
const login = getInputValue(loginInput).trim()
try {
// Скрыть предыдущие ошибки
hideElement(loginError)
// Показать состояние загрузки
loginBtn.disabled = true
loginBtn.textContent = 'Вход...'
// Выполнить вход
await authService.login(login)
console.log('Login successful')
// Скрыть login screen, показать приложение
hideLoginScreen()
setInputValue(loginInput, '')
} catch (error) {
console.error('Login failed:', error)
setElementText(loginError, error.message || 'Ошибка авторизации')
showElement(loginError)
} finally {
loginBtn.disabled = false
loginBtn.textContent = 'Войти'
}
}
/**
* Выход из системы
*/
export function handleLogout() {
if (confirm('Вы уверены, что хотите выйти?')) {
authService.logout()
}
}
/**
* Инициализация обработчиков событий
*/
export function setupListeners() {
const loginBtn = document.getElementById('login-submit-btn')
const loginInput = document.getElementById('login-input')
const logoutBtn = document.getElementById('logout-btn')
if (loginBtn) {
loginBtn.addEventListener('click', handleLogin)
}
if (loginInput) {
loginInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
handleLogin()
}
})
}
if (logoutBtn) {
logoutBtn.addEventListener('click', handleLogout)
}
}
// Export as default object
export default {
showLoginScreen,
hideLoginScreen,
handleLogin,
handleLogout,
setupListeners
}

View File

@ -1,47 +0,0 @@
/**
* Loading UI
*
* UI компонент для индикатора загрузки.
*/
import { setElementText, addClass, removeClass } from '../utils/dom.utils.js'
/**
* Показать индикатор загрузки
* @param {string} message - Сообщение для отображения
*/
export function show(message = 'Загрузка...') {
const loadingOverlay = document.getElementById('loading-overlay')
const loadingMessage = document.getElementById('loading-message')
if (!loadingOverlay) {
console.warn('Loading overlay element not found')
return
}
if (loadingMessage) {
setElementText(loadingMessage, message)
}
addClass(loadingOverlay, 'open')
}
/**
* Скрыть индикатор загрузки
*/
export function hide() {
const loadingOverlay = document.getElementById('loading-overlay')
if (!loadingOverlay) {
console.warn('Loading overlay element not found')
return
}
removeClass(loadingOverlay, 'open')
}
// Export as default object
export default {
show,
hide
}

View File

@ -1,261 +0,0 @@
/**
* Query Builder UI
*
* UI компонент для построителя запросов.
*/
import appState from '../state/appState.js'
import queryService from '../services/query.service.js'
import loadingUI from './loading.ui.js'
import { validateJSON } from '../utils/validation.utils.js'
import { addClass, removeClass, hideElement, showElement, getInputValue, setElementText } from '../utils/dom.utils.js'
import { showToast } from '../utils/dom.utils.js'
/**
* Показать построитель запросов
*/
export function show() {
const queryBuilder = document.getElementById('query-builder')
const answerViewer = document.getElementById('answer-viewer')
if (queryBuilder) {
removeClass(queryBuilder, 'hidden')
}
if (answerViewer) {
addClass(answerViewer, 'hidden')
}
}
/**
* Переключить режим запроса (questions / raw-json)
* @param {string} mode - Режим ('questions' или 'raw-json')
*/
export function switchMode(mode) {
// Update toggle buttons
const toggleButtons = document.querySelectorAll('.toggle-option')
toggleButtons.forEach(btn => {
removeClass(btn, 'active')
if (btn.dataset.mode === mode) {
addClass(btn, 'active')
}
})
// Show/hide mode panels
const questionsMode = document.getElementById('questions-mode')
const rawJsonMode = document.getElementById('raw-json-mode')
if (mode === 'questions') {
showElement(questionsMode)
hideElement(rawJsonMode)
} else if (mode === 'raw-json') {
hideElement(questionsMode)
showElement(rawJsonMode)
}
}
/**
* Валидация JSON в raw-json режиме
* @returns {boolean} True если JSON валиден
*/
export function validateJSONMode() {
const textarea = document.getElementById('json-textarea')
const message = document.getElementById('json-validation-message')
if (!textarea || !message) {
console.error('JSON textarea or validation message not found')
return false
}
const jsonText = getInputValue(textarea)
const validation = validateJSON(jsonText)
if (validation.valid) {
removeClass(textarea, 'error')
removeClass(message, 'error')
addClass(message, 'color-success')
const count = Array.isArray(validation.data) ? validation.data.length : 0
setElementText(message, `✓ JSON валиден (${count} вопросов)`)
return true
} else {
addClass(textarea, 'error')
addClass(message, 'error')
removeClass(message, 'color-success')
setElementText(message, `✗ Ошибка: ${validation.error}`)
return false
}
}
/**
* Обработать отправку запроса
* @param {Function} onSuccess - Callback при успешной отправке
*/
export async function handleSendQuery(onSuccess) {
try {
const envSettings = appState.getCurrentEnvSettings()
const currentEnvKey = appState.getCurrentEnvironment()
const apiMode = envSettings?.apiMode || 'bench'
// Get current mode from toggle
const activeToggle = document.querySelector('.toggle-option.active')
const mode = activeToggle?.dataset.mode || 'questions'
// Get form values
const questionsText = getInputValue('questions-textarea')
const jsonText = getInputValue('json-textarea')
// Build request body
const requestBody = queryService.buildRequestBody(mode, questionsText, jsonText)
// Show loading
const loadingMsg = apiMode === 'backend'
? 'Отправка запроса к Backend API...'
: 'Отправка запроса к Bench API...'
loadingUI.show(loadingMsg)
// Send query
const resetSession = envSettings?.resetSessionMode !== false
const apiResponse = await queryService.sendQuery(
currentEnvKey,
apiMode,
requestBody,
resetSession
)
// Hide loading
loadingUI.hide()
// Process response
queryService.processQueryResponse(currentEnvKey, requestBody, apiResponse)
// Call success callback
if (typeof onSuccess === 'function') {
onSuccess()
}
} catch (error) {
console.error('Query failed:', error)
loadingUI.hide()
showToast(`Ошибка запроса: ${error.message}`, 'error')
}
}
/**
* Переключить между табами
* @param {HTMLElement} tabButton - Кнопка таба
* @param {string} tabId - ID контента таба
*/
export function switchTab(tabButton, tabId) {
if (!tabButton || !tabId) {
return
}
// Get all tabs in the same group
const tabsContainer = tabButton.parentElement
const tabs = tabsContainer.querySelectorAll('.tab')
// Deactivate all tabs
tabs.forEach(tab => removeClass(tab, 'active'))
// Activate clicked tab
addClass(tabButton, 'active')
// Find and show corresponding content
const contentContainer = tabsContainer.nextElementSibling
if (contentContainer && contentContainer.classList.contains('tab-content')) {
// Handle nested tabs
const parent = tabsContainer.parentElement
const allContents = parent.querySelectorAll('.tab-content')
allContents.forEach(content => {
if (content.id === tabId) {
addClass(content, 'active')
} else if (!content.contains(tabsContainer)) {
removeClass(content, 'active')
}
})
} else {
// Handle top-level tabs
const parent = tabsContainer.parentElement
const allContents = parent.querySelectorAll(':scope > .tab-content')
allContents.forEach(content => {
if (content.id === tabId) {
addClass(content, 'active')
} else {
removeClass(content, 'active')
}
})
// If activated content has nested tabs, ensure first nested tab-content is shown
const activatedContent = document.getElementById(tabId)
if (activatedContent) {
const nestedTabsContainer = activatedContent.querySelector('.tabs')
if (nestedTabsContainer) {
// Activate first nested tab button
const nestedTabs = nestedTabsContainer.querySelectorAll('.tab')
nestedTabs.forEach((tab, index) => {
if (index === 0) {
addClass(tab, 'active')
} else {
removeClass(tab, 'active')
}
})
// Find nested tab-content elements
const children = Array.from(activatedContent.children)
const nestedContents = children.filter(el =>
el.classList.contains('tab-content') &&
el !== nestedTabsContainer
)
// Deactivate all first, then activate first one
nestedContents.forEach(content => removeClass(content, 'active'))
if (nestedContents.length > 0) {
addClass(nestedContents[0], 'active')
}
}
}
}
}
/**
* Инициализация обработчиков событий
* @param {Function} onQuerySuccess - Callback при успешной отправке запроса
*/
export function setupListeners(onQuerySuccess) {
// Toggle mode buttons
const toggleButtons = document.querySelectorAll('.toggle-option')
toggleButtons.forEach(btn => {
btn.addEventListener('click', () => {
const mode = btn.dataset.mode
if (mode) {
switchMode(mode)
}
})
})
// JSON validation
const jsonTextarea = document.getElementById('json-textarea')
if (jsonTextarea) {
jsonTextarea.addEventListener('input', validateJSONMode)
}
// Send query button
const sendQueryBtn = document.getElementById('send-query-btn')
if (sendQueryBtn) {
sendQueryBtn.addEventListener('click', () => handleSendQuery(onQuerySuccess))
}
}
// Export as default object
export default {
show,
switchMode,
validateJSONMode,
handleSendQuery,
switchTab,
setupListeners
}

View File

@ -1,166 +0,0 @@
/**
* Questions List UI
*
* UI компонент для списка вопросов.
*/
import appState from '../state/appState.js'
import { escapeHtml, formatTime, pluralize } from '../utils/format.utils.js'
import { setElementText, setElementHTML } from '../utils/dom.utils.js'
/**
* Проверить наличие аннотаций в секции документов
* @param {object} docsSection - Секция документов
* @returns {boolean} True если есть аннотации
*/
export function hasAnnotationsInDocs(docsSection) {
if (!docsSection) return false
// Check research documents
if (docsSection.research) {
for (const docIndex in docsSection.research) {
const doc = docsSection.research[docIndex]
if (doc.issues?.length > 0 || doc.comment) {
return true
}
}
}
// Check analytical_hub documents
if (docsSection.analytical_hub) {
for (const docIndex in docsSection.analytical_hub) {
const doc = docsSection.analytical_hub[docIndex]
if (doc.issues?.length > 0 || doc.comment) {
return true
}
}
}
return false
}
/**
* Отрендерить список вопросов
*/
export function render() {
const container = document.getElementById('questions-list')
const countElement = document.getElementById('questions-count')
if (!container) {
console.error('Questions list container not found')
return
}
const env = appState.getCurrentEnv()
const response = env.currentResponse
if (!response || !response.answers || response.answers.length === 0) {
setElementHTML(container, `
<div class="empty-state">
<div class="empty-state-icon">
<span class="material-icons" style="font-size: inherit;">question_answer</span>
</div>
<div class="empty-state-text">Нет данных</div>
<div class="empty-state-subtext">Отправьте запрос к RAG бэкенду</div>
</div>
`)
if (countElement) {
setElementText(countElement, '0 вопросов')
}
return
}
// Update count
if (countElement) {
const count = response.answers.length
const text = `${count} ${pluralize(count, 'вопрос', 'вопроса', 'вопросов')}`
setElementText(countElement, text)
}
// Render questions
const html = response.answers.map((answer, index) => {
const isActive = index === env.currentAnswerIndex
const annotation = env.annotations[index]
// Check for annotations in body sections
const hasBodyAnnotations = annotation && (
annotation.overall?.comment ||
annotation.body_research?.issues?.length > 0 ||
annotation.body_analytical_hub?.issues?.length > 0
)
// Check for annotations in documents
const hasDocAnnotations = annotation && (
hasAnnotationsInDocs(annotation.docs_from_vectorstore) ||
hasAnnotationsInDocs(annotation.docs_to_llm)
)
const hasAnyAnnotations = hasBodyAnnotations || hasDocAnnotations
// Get rating indicator
const rating = annotation?.overall?.rating
let ratingIndicator = ''
if (rating === 'correct') {
ratingIndicator = '<span class="material-icons" style="font-size: 18px; color: #4caf50;">check_circle</span>'
} else if (rating === 'partial') {
ratingIndicator = '<span class="material-icons" style="font-size: 18px; color: #ff9800;">error</span>'
} else if (rating === 'incorrect') {
ratingIndicator = '<span class="material-icons" style="font-size: 18px; color: #f44336;">cancel</span>'
}
// Get annotation bookmark indicator (separate from rating)
const annotationIndicator = hasAnyAnnotations
? '<span class="material-icons color-warning" style="font-size: 18px;">bookmark</span>'
: ''
return `
<div class="card card-clickable question-item ${isActive ? 'active' : ''}"
data-index="${index}"
onclick="window.selectAnswer(${index})">
<div class="card-content">
<div class="question-item-header">
<div class="text-overline">#${index + 1}</div>
<div style="display: flex; gap: 4px;">
${ratingIndicator}
${annotationIndicator}
</div>
</div>
<div class="question-text">${escapeHtml(answer.question)}</div>
<div class="question-meta">
<span>${formatTime(answer.processing_time_sec)}</span>
</div>
</div>
</div>
`
}).join('')
setElementHTML(container, html)
}
/**
* Выбрать ответ по индексу
* @param {number} index - Индекс ответа
* @param {Function} onSelect - Callback при выборе
*/
export function selectAnswer(index, onSelect) {
const env = appState.getCurrentEnv()
env.currentAnswerIndex = index
// Re-render questions list to update active state
render()
// Call callback if provided
if (typeof onSelect === 'function') {
onSelect(index)
}
}
// Export as default object
export default {
render,
selectAnswer,
hasAnnotationsInDocs
}

View File

@ -1,312 +0,0 @@
/**
* Settings UI
*
* UI компонент для диалога настроек.
*/
import appState from '../state/appState.js'
import settingsService from '../services/settings.service.js'
import { defaultSettings } from '../data/defaults.js'
import { downloadJSON, loadFileAsJSON } from '../utils/file.utils.js'
import { showToast, getInputValue, setInputValue, addClass, removeClass, showElement, hideElement } from '../utils/dom.utils.js'
/**
* Открыть диалог настроек
*/
export function open() {
populate()
const dialog = document.getElementById('settings-dialog')
if (dialog) {
addClass(dialog, 'open')
}
}
/**
* Закрыть диалог настроек
*/
export function close() {
const dialog = document.getElementById('settings-dialog')
if (dialog) {
removeClass(dialog, 'open')
}
}
/**
* Заполнить диалог настроек текущими значениями
*/
export function populate() {
const env = appState.getCurrentEnvironment()
const envSettings = appState.getCurrentEnvSettings()
if (!envSettings) {
console.error('Environment settings not found')
return
}
// Set environment selector
const envSelector = document.getElementById('settings-env-selector')
if (envSelector) {
envSelector.value = env
}
// API Mode
const apiMode = envSettings.apiMode || 'bench'
const apiModeSelect = document.getElementById('setting-api-mode')
if (apiModeSelect) {
apiModeSelect.value = apiMode
}
toggleBackendSettings(apiMode === 'backend')
// Populate environment-specific fields (только редактируемые пользователем)
setInputValue('setting-bearer-token', envSettings.bearerToken || '')
setInputValue('setting-system-platform', envSettings.systemPlatform || '')
setInputValue('setting-system-platform-user', envSettings.systemPlatformUser || '')
// Backend mode fields
setInputValue('setting-platform-user-id', envSettings.platformUserId || '')
setInputValue('setting-platform-id', envSettings.platformId || '')
const classifyCheckbox = document.getElementById('setting-with-classify')
if (classifyCheckbox) {
classifyCheckbox.checked = envSettings.withClassify || false
}
const resetSessionCheckbox = document.getElementById('setting-reset-session-mode')
if (resetSessionCheckbox) {
resetSessionCheckbox.checked = envSettings.resetSessionMode !== false
}
}
/**
* Показать/скрыть backend настройки
* @param {boolean} show - Показать или скрыть
*/
export function toggleBackendSettings(show) {
const backendSettings = document.getElementById('backend-settings')
const backendHeader = document.getElementById('backend-settings-header')
if (show) {
showElement(backendSettings)
showElement(backendHeader)
} else {
hideElement(backendSettings)
hideElement(backendHeader)
}
}
/**
* Прочитать настройки из диалога
* @returns {object} Обновлённые настройки
*/
export function read() {
const envSelector = document.getElementById('settings-env-selector')
if (!envSelector) {
console.error('Settings env selector not found')
return null
}
const env = envSelector.value
// Update environment-specific settings
const updatedSettings = JSON.parse(JSON.stringify(appState.settings)) // Deep copy
if (!updatedSettings.environments[env]) {
console.error(`Environment ${env} not found in settings`)
return null
}
updatedSettings.environments[env] = {
name: updatedSettings.environments[env].name,
apiMode: getInputValue('setting-api-mode'),
bearerToken: getInputValue('setting-bearer-token').trim(),
systemPlatform: getInputValue('setting-system-platform').trim(),
systemPlatformUser: getInputValue('setting-system-platform-user').trim(),
platformUserId: getInputValue('setting-platform-user-id').trim(),
platformId: getInputValue('setting-platform-id').trim(),
withClassify: document.getElementById('setting-with-classify')?.checked || false,
resetSessionMode: document.getElementById('setting-reset-session-mode')?.checked !== false
}
return updatedSettings
}
/**
* Сохранить настройки на сервер
*/
export async function save() {
const saveBtn = document.getElementById('save-settings-btn')
if (!saveBtn) {
console.error('Save settings button not found')
return
}
saveBtn.disabled = true
saveBtn.textContent = 'Сохранение...'
try {
const updatedSettings = read()
if (!updatedSettings) {
throw new Error('Failed to read settings from dialog')
}
await settingsService.saveToServer(updatedSettings)
showToast('Настройки сохранены на сервере', 'success')
close()
} catch (error) {
console.error('Failed to save settings:', error)
showToast(`Ошибка сохранения: ${error.message}`, 'error')
} finally {
saveBtn.disabled = false
saveBtn.textContent = 'Сохранить'
}
}
/**
* Сбросить настройки к дефолтным
*/
export async function reset() {
if (!confirm('Сбросить все настройки к значениям по умолчанию?')) {
return
}
try {
const resetSettings = { ...defaultSettings }
await settingsService.saveToServer(resetSettings)
populate()
showToast('Настройки сброшены и сохранены на сервере', 'success')
} catch (error) {
console.error('Failed to reset settings:', error)
showToast(`Ошибка сброса: ${error.message}`, 'error')
}
}
/**
* Экспортировать настройки в JSON файл
*/
export function exportSettings() {
const filename = 'brief-bench-settings.json'
downloadJSON(appState.settings, filename)
showToast('Настройки экспортированы в ' + filename, 'success')
}
/**
* Импортировать настройки из JSON файла
*/
export async function importSettings() {
const input = document.createElement('input')
input.type = 'file'
input.accept = 'application/json'
input.onchange = async (e) => {
const file = e.target.files[0]
if (!file) return
try {
const settings = await loadFileAsJSON(file)
// Validate basic structure
if (typeof settings !== 'object' || settings === null) {
throw new Error('Файл настроек должен содержать JSON объект')
}
// Merge with defaults to ensure all required fields exist
const mergedSettings = {
...defaultSettings,
...settings
}
// Save to server
await settingsService.saveToServer(mergedSettings)
populate()
showToast('Настройки импортированы и сохранены на сервере', 'success')
} catch (error) {
console.error('Failed to import settings:', error)
showToast(`Ошибка импорта: ${error.message}`, 'error')
}
}
input.click()
}
/**
* Обработчик изменения окружения в селекторе
*/
export function handleEnvironmentChange() {
populate()
}
/**
* Обработчик изменения API режима
*/
export function handleApiModeChange() {
const apiModeSelect = document.getElementById('setting-api-mode')
if (apiModeSelect) {
const apiMode = apiModeSelect.value
toggleBackendSettings(apiMode === 'backend')
}
}
/**
* Инициализация обработчиков событий
*/
export function setupListeners() {
const openBtn = document.getElementById('open-settings-btn')
const closeBtn = document.getElementById('close-settings-btn')
const saveBtn = document.getElementById('save-settings-btn')
const resetBtn = document.getElementById('reset-settings-btn')
const exportBtn = document.getElementById('export-settings-btn')
const importBtn = document.getElementById('import-settings-btn')
const envSelector = document.getElementById('settings-env-selector')
const apiModeSelect = document.getElementById('setting-api-mode')
if (openBtn) {
openBtn.addEventListener('click', open)
}
if (closeBtn) {
closeBtn.addEventListener('click', close)
}
if (saveBtn) {
saveBtn.addEventListener('click', save)
}
if (resetBtn) {
resetBtn.addEventListener('click', reset)
}
if (exportBtn) {
exportBtn.addEventListener('click', exportSettings)
}
if (importBtn) {
importBtn.addEventListener('click', importSettings)
}
if (envSelector) {
envSelector.addEventListener('change', handleEnvironmentChange)
}
if (apiModeSelect) {
apiModeSelect.addEventListener('change', handleApiModeChange)
}
}
// Export as default object
export default {
open,
close,
populate,
read,
save,
reset,
exportSettings,
importSettings,
toggleBackendSettings,
handleEnvironmentChange,
handleApiModeChange,
setupListeners
}