/**
* Brief Bench - Main Application Logic (SBS Multi-Environment Version)
* Single-page RAG testing interface with support for IFT, PSI, and PROD environments
*/
// ============================================
// Application State
// ============================================
const AppState = {
settings: null,
currentEnvironment: 'ift', // Current active environment: 'ift', 'psi', or 'prod'
// Environment-specific data
environments: {
ift: {
currentRequest: null,
currentResponse: null,
currentAnswerIndex: 0,
annotations: {},
requestTimestamp: null,
requestId: null,
},
psi: {
currentRequest: null,
currentResponse: null,
currentAnswerIndex: 0,
annotations: {},
requestTimestamp: null,
requestId: null,
},
prod: {
currentRequest: null,
currentResponse: null,
currentAnswerIndex: 0,
annotations: {},
requestTimestamp: null,
requestId: null,
}
}
};
// Helper function to get current environment state
function getCurrentEnv() {
return AppState.environments[AppState.currentEnvironment];
}
// Helper function to get current environment settings
function getCurrentEnvSettings() {
return AppState.settings.environments[AppState.currentEnvironment];
}
// ============================================
// Authentication
// ============================================
/**
* Проверить авторизацию при загрузке страницы
*/
async function checkAuth() {
if (!api.isAuthenticated()) {
showLoginScreen()
return false
}
// Попробовать загрузить настройки (валидация токена)
try {
await loadSettingsFromServer()
return true
} catch (error) {
console.error('Token validation failed:', error)
showLoginScreen()
return false
}
}
/**
* Показать экран авторизации
*/
function showLoginScreen() {
document.getElementById('login-screen').style.display = 'flex'
document.getElementById('app').style.display = 'none'
}
/**
* Скрыть экран авторизации и показать приложение
*/
function hideLoginScreen() {
document.getElementById('login-screen').style.display = 'none'
document.getElementById('app').style.display = 'block'
}
/**
* Обработка авторизации
*/
async function handleLogin() {
const loginInput = document.getElementById('login-input')
const loginError = document.getElementById('login-error')
const loginBtn = document.getElementById('login-submit-btn')
const login = loginInput.value.trim()
// Валидация
if (!/^[0-9]{8}$/.test(login)) {
loginError.textContent = 'Логин должен состоять из 8 цифр'
loginError.style.display = 'block'
return
}
loginError.style.display = 'none'
loginBtn.disabled = true
loginBtn.textContent = 'Вход...'
try {
const response = await api.login(login)
console.log('Login successful:', response.user)
// Загрузить настройки с сервера
await loadSettingsFromServer()
// Скрыть login screen, показать приложение
hideLoginScreen()
loginInput.value = ''
} catch (error) {
console.error('Login failed:', error)
loginError.textContent = error.message || 'Ошибка авторизации'
loginError.style.display = 'block'
} finally {
loginBtn.disabled = false
loginBtn.textContent = 'Войти'
}
}
/**
* Выход из системы
*/
function handleLogout() {
if (confirm('Вы уверены, что хотите выйти?')) {
api.logout()
}
}
// ============================================
// Utility Functions
// ============================================
/**
* Generate UUID v4
*/
function generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
/**
* Format time in seconds to human-readable format
*/
function formatTime(seconds) {
if (seconds < 1) {
return `${(seconds * 1000).toFixed(0)} мс`;
} else if (seconds < 60) {
return `${seconds.toFixed(2)} сек`;
} else {
const mins = Math.floor(seconds / 60);
const secs = (seconds % 60).toFixed(0);
return `${mins} мин ${secs} сек`;
}
}
/**
* Format ISO timestamp to readable format
*/
function formatTimestamp(isoString) {
const date = new Date(isoString);
return date.toLocaleString('ru-RU', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
}
/**
* Detect if text contains a table structure
*/
function isTableText(text) {
if (!text) return false;
const lines = text.split('\n');
let pipeCount = 0;
for (const line of lines) {
if (line.includes('|')) pipeCount++;
}
return pipeCount >= 2; // At least 2 lines with pipes
}
/**
* Parse text table into HTML table
*/
function parseTextTable(text) {
const lines = text.split('\n').filter(line => line.trim());
if (lines.length < 2) return null;
const rows = lines.map(line =>
line.split('|')
.map(cell => cell.trim())
.filter(cell => cell.length > 0)
);
if (rows.length === 0) return null;
// Find separator line (if exists) - typically second line with dashes
let separatorIndex = -1;
for (let i = 0; i < rows.length; i++) {
if (rows[i].every(cell => /^-+$/.test(cell.trim()))) {
separatorIndex = i;
break;
}
}
let thead = '';
let tbody = '';
if (separatorIndex === 1) {
// Header row exists
const headerCells = rows[0].map(cell => `
${escapeHtml(cell)} | `).join('');
thead = `${headerCells}
`;
const bodyRows = rows.slice(2).map(row => {
const cells = row.map(cell => `${escapeHtml(cell)} | `).join('');
return `${cells}
`;
}).join('');
tbody = `${bodyRows}`;
} else {
// No header, all rows are data
const bodyRows = rows.map(row => {
const cells = row.map(cell => `${escapeHtml(cell)} | `).join('');
return `${cells}
`;
}).join('');
tbody = `${bodyRows}`;
}
return ``;
}
/**
* Escape HTML special characters
*/
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
/**
* Download JSON file
*/
function downloadJSON(data, filename) {
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
/**
* Show toast notification (simple alert for now)
*/
function showToast(message, type = 'info') {
// TODO: Implement proper toast/snackbar component
console.log(`[${type.toUpperCase()}] ${message}`);
alert(message);
}
// ============================================
// Settings Management
// ============================================
/**
* Загрузить настройки с сервера (DB API)
*/
async function loadSettingsFromServer() {
try {
const response = await api.getSettings()
// Преобразовать в формат AppState.settings
AppState.settings = {
activeEnvironment: AppState.currentEnvironment,
environments: {
ift: {
name: 'ИФТ',
...response.settings.ift
},
psi: {
name: 'ПСИ',
...response.settings.psi
},
prod: {
name: 'ПРОМ',
...response.settings.prod
}
},
requestTimeout: 1800000, // 30 минут (фиксировано)
}
console.log('Settings loaded from server:', AppState.settings)
} catch (error) {
console.error('Failed to load settings from server:', error)
throw error
}
}
/**
* Сохранить настройки на сервер (DB API)
*/
async function saveSettingsToServer(settings) {
try {
// Извлечь только поля, которые сервер ожидает
const settingsToSave = {
ift: extractEnvironmentSettings(settings.environments.ift),
psi: extractEnvironmentSettings(settings.environments.psi),
prod: extractEnvironmentSettings(settings.environments.prod)
}
await api.updateSettings(settingsToSave)
AppState.settings = settings
console.log('Settings saved to server')
} catch (error) {
console.error('Failed to save settings to server:', error)
throw error
}
}
/**
* Извлечь только нужные поля для сервера
*/
function extractEnvironmentSettings(envSettings) {
return {
apiMode: envSettings.apiMode,
bearerToken: envSettings.bearerToken || null,
systemPlatform: envSettings.systemPlatform || null,
systemPlatformUser: envSettings.systemPlatformUser || null,
platformUserId: envSettings.platformUserId || null,
platformId: envSettings.platformId || null,
withClassify: envSettings.withClassify || false,
resetSessionMode: envSettings.resetSessionMode !== false
}
}
/**
* Populate settings dialog with current values
*/
function populateSettingsDialog() {
const env = AppState.currentEnvironment;
const envSettings = AppState.settings.environments[env];
// Set environment selector
document.getElementById('settings-env-selector').value = env;
// API Mode
const apiMode = envSettings.apiMode || 'bench';
document.getElementById('setting-api-mode').value = apiMode;
toggleBackendSettings(apiMode === 'backend');
// Populate environment-specific fields (только редактируемые пользователем)
document.getElementById('setting-bearer-token').value = envSettings.bearerToken || '';
document.getElementById('setting-system-platform').value = envSettings.systemPlatform || '';
document.getElementById('setting-system-platform-user').value = envSettings.systemPlatformUser || '';
// Backend mode fields
document.getElementById('setting-platform-user-id').value = envSettings.platformUserId || '';
document.getElementById('setting-platform-id').value = envSettings.platformId || '';
document.getElementById('setting-with-classify').checked = envSettings.withClassify || false;
document.getElementById('setting-reset-session-mode').checked = envSettings.resetSessionMode !== false;
}
/**
* Toggle visibility of backend settings
*/
function toggleBackendSettings(show) {
const backendSettings = document.getElementById('backend-settings');
const backendHeader = document.getElementById('backend-settings-header');
if (show) {
backendSettings.style.display = 'block';
backendHeader.style.display = 'block';
} else {
backendSettings.style.display = 'none';
backendHeader.style.display = 'none';
}
}
/**
* Read settings from dialog
*/
function readSettingsFromDialog() {
const env = document.getElementById('settings-env-selector').value;
// Update environment-specific settings
const updatedSettings = JSON.parse(JSON.stringify(AppState.settings)); // Deep copy
updatedSettings.environments[env] = {
name: updatedSettings.environments[env].name,
apiMode: document.getElementById('setting-api-mode').value,
bearerToken: document.getElementById('setting-bearer-token').value.trim(),
systemPlatform: document.getElementById('setting-system-platform').value.trim(),
systemPlatformUser: document.getElementById('setting-system-platform-user').value.trim(),
platformUserId: document.getElementById('setting-platform-user-id').value.trim(),
platformId: document.getElementById('setting-platform-id').value.trim(),
withClassify: document.getElementById('setting-with-classify').checked,
resetSessionMode: document.getElementById('setting-reset-session-mode').checked,
};
return updatedSettings;
}
// ============================================
// UI Initialization
// ============================================
/**
* Initialize application
*/
async function initApp() {
// Load settings from server
await loadSettingsFromServer();
AppState.currentEnvironment = AppState.settings.activeEnvironment || AppState.currentEnvironment || 'ift';
// Load saved data for each environment
['ift', 'psi', 'prod'].forEach(env => {
const savedData = localStorage.getItem(`briefBenchData_${env}`);
if (savedData) {
try {
const data = JSON.parse(savedData);
AppState.environments[env] = data;
} catch (e) {
console.error(`Failed to load data for ${env}:`, e);
}
}
});
// Load saved annotations draft for current environment (legacy support)
const savedAnnotations = localStorage.getItem('briefBenchAnnotationsDraft');
if (savedAnnotations) {
try {
const annotations = JSON.parse(savedAnnotations);
// Migrate to new structure if needed
if (!AppState.environments.ift.annotations || Object.keys(AppState.environments.ift.annotations).length === 0) {
AppState.environments.ift.annotations = annotations;
}
} catch (e) {
console.error('Failed to load annotations draft:', e);
}
}
// Setup event listeners
setupEventListeners();
// Set active environment tab
updateEnvironmentTabs();
// Show query builder by default
showQueryBuilder();
console.log('Brief Bench SBS initialized - Environment:', AppState.currentEnvironment);
}
/**
* Setup all event listeners
*/
function setupEventListeners() {
// Prevent double-binding listeners
if (AppState._eventListenersSetup) {
return;
}
AppState._eventListenersSetup = true;
// Environment tabs
document.querySelectorAll('.env-tab').forEach(tab => {
tab.addEventListener('click', (e) => {
const env = e.target.dataset.env;
switchEnvironment(env);
});
});
// Settings environment selector
document.getElementById('settings-env-selector').addEventListener('change', (e) => {
// Save current environment settings first
const currentSettings = readSettingsFromDialog();
AppState.settings = currentSettings;
// Load new environment settings
populateSettingsDialog();
});
// API Mode selector
document.getElementById('setting-api-mode').addEventListener('change', (e) => {
toggleBackendSettings(e.target.value === 'backend');
});
// App bar buttons
document.getElementById('menu-btn').addEventListener('click', toggleDrawer);
document.getElementById('new-query-btn').addEventListener('click', showQueryBuilder);
document.getElementById('settings-btn').addEventListener('click', openSettingsDialog);
document.getElementById('export-btn').addEventListener('click', exportAnalysis);
document.getElementById('import-btn').addEventListener('click', importAnalysis);
// Drawer buttons
document.getElementById('clear-all-btn').addEventListener('click', clearAll);
// Settings dialog
document.getElementById('close-settings-btn').addEventListener('click', closeSettingsDialog);
document.getElementById('save-settings-btn').addEventListener('click', saveSettingsHandler);
// Login
document.getElementById('login-submit-btn').addEventListener('click', handleLogin);
document.getElementById('login-input').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
handleLogin();
}
});
// Logout
document.getElementById('logout-btn').addEventListener('click', handleLogout);
document.getElementById('reset-settings-btn').addEventListener('click', resetSettings);
document.getElementById('import-settings-btn').addEventListener('click', importSettings);
document.getElementById('export-settings-btn').addEventListener('click', exportSettings);
// Query builder
document.querySelectorAll('.toggle-option').forEach(btn => {
btn.addEventListener('click', (e) => {
const mode = e.target.dataset.mode;
switchQueryMode(mode);
});
});
document.getElementById('validate-json-btn').addEventListener('click', validateJSON);
document.getElementById('send-query-btn').addEventListener('click', handleSendQuery);
document.getElementById('load-response-btn').addEventListener('click', loadResponseFromFile);
document.getElementById('load-request-btn').addEventListener('click', loadRequestFromFile);
// Tab navigation
document.addEventListener('click', (e) => {
if (e.target.classList.contains('tab')) {
const tabId = e.target.dataset.tab;
if (tabId) {
switchTab(e.target, tabId);
}
}
});
// Annotation changes
document.addEventListener('change', (e) => {
if (e.target.classList.contains('checkbox') ||
e.target.tagName === 'SELECT' ||
e.target.tagName === 'TEXTAREA') {
saveAnnotationsDraft();
}
});
// Close dialog on overlay click
document.getElementById('settings-dialog').addEventListener('click', (e) => {
if (e.target.id === 'settings-dialog') {
closeSettingsDialog();
}
});
}
/**
* Switch to a different environment
*/
function switchEnvironment(env) {
// Save current environment data
saveEnvironmentData(AppState.currentEnvironment);
// Switch environment
AppState.currentEnvironment = env;
AppState.settings.activeEnvironment = env;
// activeEnvironment - это локальное состояние UI, не сохраняем на сервер
// Update UI
updateEnvironmentTabs();
// Reload content for new environment
const currentEnv = getCurrentEnv();
if (currentEnv.currentResponse) {
renderQuestionsList();
renderAnswer(currentEnv.currentAnswerIndex);
} else {
showQueryBuilder();
}
console.log('Switched to environment:', env);
}
/**
* Update environment tabs visual state
*/
function updateEnvironmentTabs() {
document.querySelectorAll('.env-tab').forEach(tab => {
if (tab.dataset.env === AppState.currentEnvironment) {
tab.classList.add('active');
} else {
tab.classList.remove('active');
}
});
}
/**
* Save environment data to localStorage
*/
function saveEnvironmentData(env) {
const data = AppState.environments[env];
localStorage.setItem(`briefBenchData_${env}`, JSON.stringify(data));
}
/**
* Toggle drawer visibility (mobile)
*/
function toggleDrawer() {
const drawer = document.getElementById('drawer');
drawer.classList.toggle('collapsed');
}
/**
* Clear all data and reload page
*/
function clearAll() {
if (confirm('Очистить все данные и обновить страницу? Несохраненные изменения будут потеряны.')) {
window.location.reload();
}
}
/**
* Switch query mode
*/
function switchQueryMode(mode) {
document.querySelectorAll('.toggle-option').forEach(btn => {
btn.classList.remove('active');
if (btn.dataset.mode === mode) {
btn.classList.add('active');
}
});
document.getElementById('questions-mode').classList.toggle('hidden', mode !== 'questions');
document.getElementById('raw-json-mode').classList.toggle('hidden', mode !== 'raw-json');
}
/**
* Switch between tabs
*/
function switchTab(tabButton, tabId) {
// Get all tabs in the same group
const tabsContainer = tabButton.parentElement;
const tabs = tabsContainer.querySelectorAll('.tab');
// Deactivate all tabs
tabs.forEach(tab => tab.classList.remove('active'));
// Activate clicked tab
tabButton.classList.add('active');
// Find and show corresponding content
const contentContainer = tabsContainer.nextElementSibling;
if (contentContainer && contentContainer.classList.contains('tab-content')) {
// Handle nested tabs
let parent = tabsContainer.parentElement;
const allContents = parent.querySelectorAll('.tab-content');
allContents.forEach(content => {
if (content.id === tabId) {
content.classList.add('active');
} else if (!content.contains(tabsContainer)) {
content.classList.remove('active');
}
});
} else {
// Handle top-level tabs
const parent = tabsContainer.parentElement;
const allContents = parent.querySelectorAll(':scope > .tab-content');
allContents.forEach(content => {
content.classList.toggle('active', content.id === tabId);
});
// 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) {
tab.classList.add('active');
} else {
tab.classList.remove('active');
}
});
// Find nested tab-content elements (immediate children only, after tabs container)
// Use children to get elements in correct DOM order
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 => content.classList.remove('active'));
if (nestedContents.length > 0) {
nestedContents[0].classList.add('active');
}
}
}
}
}
// ============================================
// Settings Dialog
// ============================================
function openSettingsDialog() {
populateSettingsDialog();
document.getElementById('settings-dialog').classList.add('open');
}
function closeSettingsDialog() {
document.getElementById('settings-dialog').classList.remove('open');
}
async function saveSettingsHandler() {
const saveBtn = document.getElementById('save-settings-btn');
saveBtn.disabled = true;
saveBtn.textContent = 'Сохранение...';
try {
const updatedSettings = readSettingsFromDialog();
await saveSettingsToServer(updatedSettings);
showToast('Настройки сохранены на сервере', 'success');
closeSettingsDialog();
} catch (error) {
console.error('Failed to save settings:', error);
showToast(`Ошибка сохранения: ${error.message}`, 'error');
} finally {
saveBtn.disabled = false;
saveBtn.textContent = 'Сохранить';
}
}
async function resetSettings() {
if (confirm('Сбросить все настройки к значениям по умолчанию?')) {
try {
AppState.settings = { ...defaultSettings };
await saveSettingsToServer(AppState.settings);
populateSettingsDialog();
showToast('Настройки сброшены и сохранены на сервере', 'success');
} catch (error) {
console.error('Failed to reset settings:', error);
showToast(`Ошибка сброса: ${error.message}`, 'error');
}
}
}
function exportSettings() {
const filename = 'brief-bench-settings.json';
downloadJSON(AppState.settings, filename);
showToast('Настройки экспортированы в ' + filename, 'success');
}
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 text = await file.text();
const settings = JSON.parse(text);
// Validate basic structure
if (typeof settings !== 'object' || settings === null) {
throw new Error('Файл настроек должен содержать JSON объект');
}
// Merge with defaults to ensure all required fields exist
AppState.settings = {
...defaultSettings,
...settings
};
// Save to server
await saveSettingsToServer(AppState.settings);
// Update dialog
populateSettingsDialog();
showToast('Настройки успешно импортированы и сохранены на сервере', 'success');
} catch (error) {
showToast(`Ошибка импорта настроек: ${error.message}`, 'error');
}
};
input.click();
}
// ============================================
// Query Builder
// ============================================
function showQueryBuilder() {
document.getElementById('query-builder').classList.remove('hidden');
document.getElementById('answer-viewer').classList.add('hidden');
}
function validateJSON() {
const textarea = document.getElementById('json-textarea');
const message = document.getElementById('json-validation-message');
try {
const json = JSON.parse(textarea.value);
// Validate schema
if (!Array.isArray(json)) {
throw new Error('JSON должен быть массивом');
}
for (let i = 0; i < json.length; i++) {
const item = json[i];
if (typeof item.body !== 'string') {
throw new Error(`Элемент ${i}: поле "body" должно быть строкой`);
}
if (typeof item.with_docs !== 'boolean') {
throw new Error(`Элемент ${i}: поле "with_docs" должно быть boolean`);
}
}
textarea.classList.remove('error');
message.textContent = `✓ JSON валиден (${json.length} вопросов)`;
message.classList.remove('error');
message.classList.add('color-success');
return true;
} catch (error) {
textarea.classList.add('error');
message.textContent = `✗ Ошибка: ${error.message}`;
message.classList.add('error');
message.classList.remove('color-success');
return false;
}
}
function buildRequestBody() {
const mode = document.querySelector('.toggle-option.active').dataset.mode;
if (mode === 'questions') {
const text = document.getElementById('questions-textarea').value;
const questions = text.split('\n')
.map(line => line.trim())
.filter(line => line.length > 0);
if (questions.length === 0) {
throw new Error('Введите хотя бы один вопрос');
}
return questions.map(q => ({
body: q,
with_docs: AppState.settings.defaultWithDocs
}));
} else {
const json = document.getElementById('json-textarea').value;
return JSON.parse(json);
}
}
async function handleSendQuery() {
try {
const envSettings = getCurrentEnvSettings();
const env = getCurrentEnv();
const apiMode = envSettings.apiMode || 'bench';
const requestBody = buildRequestBody();
env.currentRequest = requestBody;
// Show loading
const loadingMsg = apiMode === 'backend'
? 'Отправка запроса к Backend API...'
: 'Отправка запроса к Bench API...';
showLoading(loadingMsg);
const currentEnvKey = AppState.currentEnvironment;
let apiResponse;
if (apiMode === 'bench') {
apiResponse = await api.benchQuery(currentEnvKey, requestBody);
// Response format: { request_id, timestamp, environment, response }
env.currentResponse = apiResponse.response;
env.requestId = apiResponse.request_id;
env.requestTimestamp = apiResponse.timestamp;
} else if (apiMode === 'backend') {
const resetSession = envSettings.resetSessionMode !== false;
apiResponse = await api.backendQuery(currentEnvKey, requestBody, resetSession);
// Response format: { request_id, timestamp, environment, response }
env.currentResponse = apiResponse.response;
env.requestId = apiResponse.request_id;
env.requestTimestamp = apiResponse.timestamp;
} else {
throw new Error(`Неизвестный режим API: ${apiMode}`);
}
// Hide loading
hideLoading();
// Validate response
if (!env.currentResponse || !env.currentResponse.answers || !Array.isArray(env.currentResponse.answers)) {
throw new Error('Некорректный формат ответа: отсутствует поле "answers"');
}
env.currentAnswerIndex = 0;
// Initialize annotations for new response
env.annotations = {};
// Save to localStorage
saveEnvironmentData(AppState.currentEnvironment);
// Render UI
renderQuestionsList();
renderAnswer(0);
const modeLabel = apiMode === 'backend' ? 'Backend' : 'Bench';
showToast(`[${modeLabel}] Получено ${env.currentResponse.answers.length} ответов`, 'success');
} catch (error) {
hideLoading();
showToast(`Ошибка: ${error.message}`, 'error');
console.error('Query error:', error);
}
}
/**
* Extract questions from query builder
*/
function extractQuestions() {
const mode = document.querySelector('.toggle-option.active')?.dataset.mode || 'questions';
if (mode === 'questions') {
const text = document.getElementById('questions-textarea').value.trim();
if (!text) {
throw new Error('Введите хотя бы один вопрос');
}
// Split by newlines and filter empty lines
return text.split('\n').filter(line => line.trim().length > 0).map(line => line.trim());
} else {
// Raw JSON mode
const json = document.getElementById('json-textarea').value;
const parsed = JSON.parse(json);
if (Array.isArray(parsed)) {
// If it's array of objects with 'body', extract bodies
if (parsed.length > 0 && typeof parsed[0] === 'object' && parsed[0].body) {
return parsed.map(item => item.body);
}
// If it's array of strings
if (parsed.length > 0 && typeof parsed[0] === 'string') {
return parsed;
}
}
throw new Error('JSON должен быть массивом вопросов (строк или объектов с полем "body")');
}
}
function loadResponseFromFile() {
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 text = await file.text();
const data = JSON.parse(text);
// Validate response format
if (!data.answers || !Array.isArray(data.answers)) {
throw new Error('Файл должен содержать объект с полем "answers" (массив)');
}
const env = getCurrentEnv();
// Set response
env.currentResponse = data;
env.currentAnswerIndex = 0;
env.requestTimestamp = new Date().toISOString();
env.requestId = 'loaded-' + generateUUID();
env.annotations = {};
// Try to reconstruct request from questions in response
env.currentRequest = data.answers.map(answer => ({
body: answer.question,
with_docs: true
}));
// Save to localStorage
saveEnvironmentData(AppState.currentEnvironment);
renderQuestionsList();
renderAnswer(0);
showToast(`Загружен ответ: ${data.answers.length} вопросов`, 'success');
} catch (error) {
showToast(`Ошибка загрузки ответа: ${error.message}`, 'error');
}
};
input.click();
}
function loadRequestFromFile() {
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 text = await file.text();
const data = JSON.parse(text);
// Validate it's an array
if (!Array.isArray(data)) {
throw new Error('Файл должен содержать JSON массив');
}
// Load into textarea
document.getElementById('json-textarea').value = JSON.stringify(data, null, 2);
validateJSON();
showToast(`Загружен запрос: ${data.length} вопросов`, 'success');
} catch (error) {
showToast(`Ошибка загрузки запроса: ${error.message}`, 'error');
}
};
input.click();
}
function importAnalysis() {
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 text = await file.text();
const data = JSON.parse(text);
// Validate it's a full export (analysis file)
if (!data.request || !data.response || !data.response.answers) {
throw new Error('Неверный формат файла анализа. Используйте файл, экспортированный через "Экспорт анализа".');
}
// Determine target environment (default to current if not specified - backward compatibility)
const targetEnv = data.environment || AppState.currentEnvironment;
// Check if we need to switch environment
if (targetEnv !== AppState.currentEnvironment) {
// Save current environment before switching
saveEnvironmentData(AppState.currentEnvironment);
// Switch to target environment
AppState.currentEnvironment = targetEnv;
AppState.settings.activeEnvironment = targetEnv;
// activeEnvironment - это локальное состояние UI, не сохраняем на сервер
updateEnvironmentTabs();
}
const env = getCurrentEnv();
// Restore complete state to target environment
env.currentRequest = data.request;
env.currentResponse = data.response;
env.annotations = data.annotations || {};
env.requestTimestamp = data.settings_snapshot?.timestamp || data.exported_at || new Date().toISOString();
env.requestId = data.settings_snapshot?.requestId || generateUUID();
env.currentAnswerIndex = 0;
// Save to localStorage
saveEnvironmentData(targetEnv);
// Render UI
renderQuestionsList();
renderAnswer(0);
// Count annotations
const annotationCount = Object.keys(env.annotations).filter(key => {
const ann = env.annotations[key];
return ann.overall?.rating || ann.overall?.comment ||
ann.body_research?.issues?.length > 0 ||
ann.body_analytical_hub?.issues?.length > 0;
}).length;
const envName = AppState.settings.environments[targetEnv].name;
const message = `Анализ импортирован в ${envName}: ${data.response.answers.length} вопросов` +
(annotationCount > 0 ? `, ${annotationCount} с пометками` : '');
showToast(message, 'success');
} catch (error) {
showToast(`Ошибка импорта: ${error.message}`, 'error');
}
};
input.click();
}
// ============================================
// Loading Overlay
// ============================================
function showLoading(message) {
document.getElementById('loading-message').textContent = message;
document.getElementById('loading-overlay').classList.add('open');
}
function hideLoading() {
document.getElementById('loading-overlay').classList.remove('open');
}
// ============================================
// Questions List Rendering
// ============================================
/**
* Check if there are annotations in document sections
*/
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;
}
function renderQuestionsList() {
const container = document.getElementById('questions-list');
const env = getCurrentEnv();
const response = env.currentResponse;
if (!response || !response.answers || response.answers.length === 0) {
container.innerHTML = `
question_answer
Нет данных
Отправьте запрос к RAG бэкенду
`;
document.getElementById('questions-count').textContent = '0 вопросов';
return;
}
document.getElementById('questions-count').textContent =
`${response.answers.length} ${pluralize(response.answers.length, 'вопрос', 'вопроса', 'вопросов')}`;
container.innerHTML = 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 = 'check_circle';
} else if (rating === 'partial') {
ratingIndicator = 'error';
} else if (rating === 'incorrect') {
ratingIndicator = 'cancel';
}
// Get annotation bookmark indicator (separate from rating)
const annotationIndicator = hasAnyAnnotations
? 'bookmark'
: '';
return `
${escapeHtml(answer.question)}
${formatTime(answer.processing_time_sec)}
`;
}).join('');
}
function pluralize(count, one, few, many) {
const mod10 = count % 10;
const mod100 = count % 100;
if (mod10 === 1 && mod100 !== 11) return one;
if (mod10 >= 2 && mod10 <= 4 && (mod100 < 10 || mod100 >= 20)) return few;
return many;
}
function selectAnswer(index) {
const env = getCurrentEnv();
env.currentAnswerIndex = index;
renderQuestionsList();
renderAnswer(index);
}
// ============================================
// Answer Rendering
// ============================================
function renderAnswer(index) {
const env = getCurrentEnv();
const answer = env.currentResponse.answers[index];
if (!answer) return;
const isBackendMode = answer.backend_mode === true;
// Show answer viewer, hide query builder
document.getElementById('query-builder').classList.add('hidden');
document.getElementById('answer-viewer').classList.remove('hidden');
// Render question header
document.getElementById('current-question-number').textContent = index + 1;
document.getElementById('current-question-text').textContent = answer.question;
// Render metadata
document.getElementById('processing-time').textContent = isBackendMode ? 'N/A' : formatTime(answer.processing_time_sec);
document.getElementById('request-id').textContent = env.requestId || '-';
document.getElementById('request-timestamp').textContent =
env.requestTimestamp ? formatTimestamp(env.requestTimestamp) : '-';
// Render answer bodies
renderAnswerBody('body-research-text', answer.body_research);
renderAnswerBody('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) {
docsSection.style.display = isBackendMode ? 'none' : 'block';
}
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
loadAnnotationsForAnswer(index);
}
function renderAnswerBody(elementId, text) {
const container = document.getElementById(elementId);
if (!text) {
container.innerHTML = 'Нет данных
';
return;
}
if (isTableText(text)) {
const table = parseTextTable(text);
if (table) {
container.innerHTML = `${table}
`;
return;
}
}
// Render as plain text with line breaks
container.innerHTML = `${escapeHtml(text).replace(/\n/g, '
')}
`;
}
function renderDocuments(containerId, docs, section, subsection, answerIndex) {
const container = document.getElementById(containerId);
if (!docs || docs.length === 0) {
container.innerHTML = `
`;
return;
}
container.innerHTML = 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 || `${escapeHtml(doc)}`;
} else {
docContent = `${escapeHtml(doc).replace(/\n/g, '
')}
`;
}
} else {
docContent = `${escapeHtml(JSON.stringify(doc, null, 2))}`;
}
return `
`;
}).join('');
}
function toggleExpansion(id) {
const panel = document.getElementById(id);
panel.classList.toggle('expanded');
}
// ============================================
// Annotation System
// ============================================
function initAnnotationForAnswer(index) {
const env = 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: {} }
};
}
}
function loadAnnotationsForAnswer(index) {
initAnnotationForAnswer(index);
const env = getCurrentEnv();
const annotation = env.annotations[index];
// Load overall rating
document.getElementById('overall-rating').value = annotation.overall.rating || '';
document.getElementById('overall-comment').value = annotation.overall.comment || '';
// Load body annotations
loadSectionAnnotation('body_research', annotation.body_research);
loadSectionAnnotation('body_analytical_hub', annotation.body_analytical_hub);
// Load document annotations
loadDocumentAnnotations('docs_from_vectorstore', 'research', annotation.docs_from_vectorstore?.research);
loadDocumentAnnotations('docs_from_vectorstore', 'analytical_hub', annotation.docs_from_vectorstore?.analytical_hub);
loadDocumentAnnotations('docs_to_llm', 'research', annotation.docs_to_llm?.research);
loadDocumentAnnotations('docs_to_llm', 'analytical_hub', annotation.docs_to_llm?.analytical_hub);
// Setup event listeners for current answer
setupAnnotationListeners();
}
function loadSectionAnnotation(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) {
textarea.value = data.comment || '';
}
}
function loadDocumentAnnotations(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) {
textarea.value = data.comment || '';
}
});
}
function setupAnnotationListeners() {
const env = getCurrentEnv();
const index = env.currentAnswerIndex;
// Overall rating
document.getElementById('overall-rating').onchange = (e) => {
env.annotations[index].overall.rating = e.target.value;
saveAnnotationsDraft();
};
document.getElementById('overall-comment').oninput = (e) => {
env.annotations[index].overall.comment = e.target.value;
saveAnnotationsDraft();
};
// 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);
saveAnnotationsDraft();
};
} else if (element.tagName === 'TEXTAREA') {
element.oninput = (e) => {
if (docIndex !== undefined) {
if (!env.annotations[index][section][subsection][docIndex]) {
env.annotations[index][section][subsection][docIndex] = { issues: [], comment: '' };
}
env.annotations[index][section][subsection][docIndex].comment = e.target.value;
} else if (section !== 'overall') {
env.annotations[index][section].comment = e.target.value;
}
saveAnnotationsDraft();
};
}
});
}
function updateCheckboxStyle(checkbox) {
const label = checkbox.closest('.issue-checkbox');
if (label) {
label.classList.toggle('checked', checkbox.checked);
}
}
function saveAnnotationsDraft() {
// Save annotations and entire environment state
saveEnvironmentData(AppState.currentEnvironment);
}
// ============================================
// Export Functionality
// ============================================
function exportAnalysis() {
const env = getCurrentEnv();
if (!env.currentResponse) {
showToast('Нет данных для экспорта', 'warning');
return;
}
const envSettings = getCurrentEnvSettings();
const envName = AppState.settings.environments[AppState.currentEnvironment].name;
const exportData = {
environment: AppState.currentEnvironment, // Save environment info
api_mode: envSettings.apiMode || 'bench', // Save API mode
request: env.currentRequest,
response: env.currentResponse,
annotations: env.annotations,
settings_snapshot: {
requestId: env.requestId,
systemId: envSettings.systemId,
timestamp: env.requestTimestamp,
environment_name: envName,
api_mode: envSettings.apiMode || 'bench',
},
exported_at: new Date().toISOString(),
};
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
const filename = `brief-bench-analysis-${AppState.currentEnvironment}-${timestamp}.json`;
downloadJSON(exportData, filename);
showToast(`Анализ экспортирован: ${filename}`, 'success');
}
// ============================================
// Initialize on Load
// ============================================
// Initialize app on load
document.addEventListener('DOMContentLoaded', async () => {
// Подключить обработчики (в т.ч. для login/logout)
setupEventListeners();
// Проверить авторизацию
const isAuthenticated = await checkAuth();
if (isAuthenticated) {
// Пользователь авторизован, инициализировать приложение
await initApp();
}
// Если не авторизован, login screen уже показан в checkAuth()
});