Compare commits
No commits in common. "9997897411b566efff93f16e88dda3b80dbc3137" and "fc2eeb82ca218b0eb1e72f878a119d10c26b2564" have entirely different histories.
9997897411
...
fc2eeb82ca
|
|
@ -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 завершён)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
Loading…
Reference in New Issue