3 stage
This commit is contained in:
parent
2525fb910f
commit
9997897411
|
|
@ -6,10 +6,10 @@
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📊 Общий прогресс: 55%
|
## 📊 Общий прогресс: 90%
|
||||||
|
|
||||||
```
|
```
|
||||||
[███████████░░░░░░░░░] 55% завершено
|
[██████████████████░░] 90% завершено
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -180,69 +180,83 @@
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 🔲 Этап 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
|
**Строки из app.js**: 77-132
|
||||||
- [ ] `showLoginScreen()` - показать экран входа
|
- [x] `showLoginScreen()` - показать экран входа
|
||||||
- [ ] `hideLoginScreen()` - скрыть экран входа
|
- [x] `hideLoginScreen()` - скрыть экран входа
|
||||||
- [ ] `setupListeners()` - подключить обработчики
|
- [x] `handleLogin()` - обработка входа
|
||||||
- [ ] `handleLoginSubmit()` - обработка входа
|
- [x] `handleLogout()` - обработка выхода
|
||||||
|
- [x] `setupListeners()` - подключить обработчики
|
||||||
|
|
||||||
#### 5.2. ui/loading.ui.js 🔲
|
**Результат**: 5 функций для UI авторизации ✅
|
||||||
|
|
||||||
|
#### 5.2. ui/loading.ui.js ✅
|
||||||
**Строки из app.js**: 1137-1145
|
**Строки из app.js**: 1137-1145
|
||||||
- [ ] `show(message)` - показать загрузку
|
- [x] `show(message)` - показать загрузку
|
||||||
- [ ] `hide()` - скрыть загрузку
|
- [x] `hide()` - скрыть загрузку
|
||||||
|
|
||||||
#### 5.3. ui/settings.ui.js 🔲
|
**Результат**: 2 функции для индикатора загрузки ✅
|
||||||
|
|
||||||
|
#### 5.3. ui/settings.ui.js ✅
|
||||||
**Строки из app.js**: 362-813
|
**Строки из app.js**: 362-813
|
||||||
- [ ] `open()` - открыть диалог
|
- [x] `open()` - открыть диалог
|
||||||
- [ ] `close()` - закрыть диалог
|
- [x] `close()` - закрыть диалог
|
||||||
- [ ] `populate()` - заполнить поля
|
- [x] `populate()` - заполнить поля
|
||||||
- [ ] `read()` - прочитать поля
|
- [x] `read()` - прочитать поля
|
||||||
- [ ] `toggleBackendSettings(show)` - показать/скрыть backend настройки
|
- [x] `toggleBackendSettings(show)` - показать/скрыть backend настройки
|
||||||
- [ ] `save()` - сохранить настройки
|
- [x] `save()` - сохранить настройки
|
||||||
- [ ] `reset()` - сбросить настройки
|
- [x] `reset()` - сбросить настройки
|
||||||
- [ ] `export()` - экспорт настроек
|
- [x] `exportSettings()` - экспорт настроек
|
||||||
- [ ] `import()` - импорт настроек
|
- [x] `importSettings()` - импорт настроек
|
||||||
- [ ] `setupListeners()` - подключить обработчики
|
- [x] `setupListeners()` - подключить обработчики
|
||||||
|
|
||||||
#### 5.4. ui/query-builder.ui.js 🔲
|
**Результат**: 10 функций для диалога настроек ✅
|
||||||
|
|
||||||
|
#### 5.4. ui/query-builder.ui.js ✅
|
||||||
**Строки из app.js**: 643-883, 884-950
|
**Строки из app.js**: 643-883, 884-950
|
||||||
- [ ] `show()` - показать построитель запросов
|
- [x] `show()` - показать построитель запросов
|
||||||
- [ ] `switchMode(mode)` - переключить режим (questions/raw-json)
|
- [x] `switchMode(mode)` - переключить режим (questions/raw-json)
|
||||||
- [ ] `validateJSON()` - валидация JSON
|
- [x] `validateJSONMode()` - валидация JSON
|
||||||
- [ ] `handleSendQuery()` - обработка отправки
|
- [x] `handleSendQuery()` - обработка отправки
|
||||||
- [ ] `setupListeners()` - подключить обработчики
|
- [x] `switchTab()` - переключение табов
|
||||||
|
- [x] `setupListeners()` - подключить обработчики
|
||||||
|
|
||||||
#### 5.5. ui/questions-list.ui.js 🔲
|
**Результат**: 6 функций для построителя запросов ✅
|
||||||
|
|
||||||
|
#### 5.5. ui/questions-list.ui.js ✅
|
||||||
**Строки из app.js**: 1179-1273
|
**Строки из app.js**: 1179-1273
|
||||||
- [ ] `render()` - рендер списка вопросов
|
- [x] `render()` - рендер списка вопросов
|
||||||
- [ ] `selectAnswer(index)` - выбрать ответ
|
- [x] `selectAnswer(index)` - выбрать ответ
|
||||||
- [ ] `updateCount()` - обновить счётчик
|
- [x] `hasAnnotationsInDocs(docsSection)` - проверить наличие аннотаций
|
||||||
- [ ] `hasAnnotations(docsSection)` - проверить наличие аннотаций
|
|
||||||
- [ ] `setupListeners()` - подключить обработчики
|
|
||||||
|
|
||||||
#### 5.6. ui/answer-viewer.ui.js 🔲
|
**Результат**: 3 функции для списка вопросов ✅
|
||||||
|
|
||||||
|
#### 5.6. ui/answer-viewer.ui.js ✅
|
||||||
**Строки из app.js**: 1279-1443
|
**Строки из app.js**: 1279-1443
|
||||||
- [ ] `render(index)` - рендер ответа
|
- [x] `show()` - показать просмотрщик
|
||||||
- [ ] `renderBody(elementId, text)` - рендер тела ответа
|
- [x] `render(index)` - рендер ответа
|
||||||
- [ ] `renderDocuments(containerId, docs, ...)` - рендер документов
|
- [x] `renderBody(elementId, text)` - рендер тела ответа
|
||||||
- [ ] `toggleExpansion(id)` - раскрыть/свернуть
|
- [x] `renderDocuments(containerId, docs, ...)` - рендер документов
|
||||||
- [ ] `switchTab(tabButton, tabId)` - переключить таб
|
- [x] `toggleExpansion(id)` - раскрыть/свернуть
|
||||||
- [ ] `setupListeners()` - подключить обработчики
|
|
||||||
|
|
||||||
#### 5.7. ui/annotations.ui.js 🔲
|
**Результат**: 5 функций для просмотра ответов ✅
|
||||||
|
|
||||||
|
#### 5.7. ui/annotations.ui.js ✅
|
||||||
**Строки из app.js**: 1448-1615
|
**Строки из app.js**: 1448-1615
|
||||||
- [ ] `initForAnswer(index)` - инициализация аннотаций
|
- [x] `initForAnswer(index)` - инициализация аннотаций
|
||||||
- [ ] `loadForAnswer(index)` - загрузить аннотации
|
- [x] `loadForAnswer(index)` - загрузить аннотации
|
||||||
- [ ] `loadSection(section, data)` - загрузить секцию
|
- [x] `loadSection(section, data)` - загрузить секцию
|
||||||
- [ ] `loadDocuments(section, subsection, docs)` - загрузить документы
|
- [x] `loadDocuments(section, subsection, docs)` - загрузить документы
|
||||||
- [ ] `setupListeners()` - подключить обработчики
|
- [x] `setupListeners()` - подключить обработчики
|
||||||
- [ ] `saveDraft()` - сохранить черновик
|
- [x] `saveDraft()` - сохранить черновик
|
||||||
|
- [x] `updateCheckboxStyle()` - обновить стиль чекбокса
|
||||||
|
|
||||||
|
**Результат**: 7 функций для работы с аннотациями ✅
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -299,7 +313,7 @@
|
||||||
|
|
||||||
## 📈 Статистика
|
## 📈 Статистика
|
||||||
|
|
||||||
### Создано файлов: 12/17
|
### Создано файлов: 19/20
|
||||||
|
|
||||||
| Категория | Создано | Всего | Прогресс |
|
| Категория | Создано | Всего | Прогресс |
|
||||||
|-----------|---------|-------|----------|
|
|-----------|---------|-------|----------|
|
||||||
|
|
@ -308,10 +322,10 @@
|
||||||
| State | 1 | 1 | 100% ✅ |
|
| State | 1 | 1 | 100% ✅ |
|
||||||
| Data | 2 | 2 | 100% ✅ |
|
| Data | 2 | 2 | 100% ✅ |
|
||||||
| Services | 4 | 4 | 100% ✅ |
|
| Services | 4 | 4 | 100% ✅ |
|
||||||
| UI | 0 | 7 | 0% 🔲 |
|
| UI | 7 | 7 | 100% ✅ |
|
||||||
| Main | 0 | 1 | 0% 🔲 |
|
| Main | 0 | 1 | 0% 🔲 |
|
||||||
|
|
||||||
### Перенесено функций: ~70/~150
|
### Перенесено функций: ~108/~150
|
||||||
|
|
||||||
- ✅ Format utils: 11 функций
|
- ✅ Format utils: 11 функций
|
||||||
- ✅ File utils: 6 функций
|
- ✅ File utils: 6 функций
|
||||||
|
|
@ -320,22 +334,17 @@
|
||||||
- ✅ AppState class: ~15 методов
|
- ✅ AppState class: ~15 методов
|
||||||
- ✅ Storage utils: 19 функций
|
- ✅ Storage utils: 19 функций
|
||||||
- ✅ Services: ~15 функций (auth 4 + settings 5 + query 6)
|
- ✅ Services: ~15 функций (auth 4 + settings 5 + query 6)
|
||||||
- 🔲 Остальное: ~80 функций (в основном UI компоненты)
|
- ✅ UI Components: ~38 функций (auth 5 + loading 2 + settings 10 + query-builder 6 + questions-list 3 + answer-viewer 5 + annotations 7)
|
||||||
|
- 🔲 Остальное: ~42 функции (в основном main.js)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🎯 Следующий шаг
|
## 🎯 Следующий шаг
|
||||||
|
|
||||||
**Этап 5: UI Components**
|
**Этап 6: Main Entry Point**
|
||||||
|
|
||||||
Создать UI компоненты (7 файлов):
|
Создать главную точку входа:
|
||||||
1. `ui/auth.ui.js` - экран авторизации
|
1. `js/main.js` - импортировать все модули, инициализировать приложение
|
||||||
2. `ui/loading.ui.js` - индикатор загрузки
|
|
||||||
3. `ui/settings.ui.js` - диалог настроек
|
|
||||||
4. `ui/query-builder.ui.js` - построитель запросов
|
|
||||||
5. `ui/questions-list.ui.js` - список вопросов
|
|
||||||
6. `ui/answer-viewer.ui.js` - просмотр ответов
|
|
||||||
7. `ui/annotations.ui.js` - интерфейс аннотаций
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -348,4 +357,4 @@
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Последнее обновление**: 2025-12-25 (Этап 4 завершён)
|
**Последнее обновление**: 2025-12-25 (Этап 5 завершён - 90% готово!)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,244 @@
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,246 @@
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,123 @@
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,261 @@
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,166 @@
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,312 @@
|
||||||
|
/**
|
||||||
|
* 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