This commit is contained in:
itqop 2025-12-25 11:47:11 +03:00
parent 2525fb910f
commit 9997897411
8 changed files with 1472 additions and 64 deletions

View File

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

View File

@ -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
}

View File

@ -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
}

123
static/js/ui/auth.ui.js Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

312
static/js/ui/settings.ui.js Normal file
View File

@ -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
}