brief-rags-bench/static/app.js.bak

1895 lines
59 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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 => `<th>${escapeHtml(cell)}</th>`).join('');
thead = `<thead><tr>${headerCells}</tr></thead>`;
const bodyRows = rows.slice(2).map(row => {
const cells = row.map(cell => `<td>${escapeHtml(cell)}</td>`).join('');
return `<tr>${cells}</tr>`;
}).join('');
tbody = `<tbody>${bodyRows}</tbody>`;
} else {
// No header, all rows are data
const bodyRows = rows.map(row => {
const cells = row.map(cell => `<td>${escapeHtml(cell)}</td>`).join('');
return `<tr>${cells}</tr>`;
}).join('');
tbody = `<tbody>${bodyRows}</tbody>`;
}
return `<table class="rendered-table">${thead}${tbody}</table>`;
}
/**
* 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 || '',
systemPlatform: envSettings.systemPlatform || '',
systemPlatformUser: envSettings.systemPlatformUser || '',
platformUserId: envSettings.platformUserId || '',
platformId: envSettings.platformId || '',
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;
}
// ============================================
// API Client
// ============================================
/**
* Build API URL from current environment settings
*/
function buildApiUrl() {
const envSettings = getCurrentEnvSettings();
if (envSettings.fullUrl) {
return envSettings.fullUrl;
}
const { host, port, endpoint } = envSettings;
const portPart = port ? `:${port}` : '';
const endpointPart = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
return `https://${host}${portPart}${endpointPart}`;
}
/**
* Generate Request ID for current environment
*/
function generateRequestId() {
const envSettings = getCurrentEnvSettings();
const template = envSettings.requestIdTemplate;
if (template === 'uuid' || !template) {
return generateUUID();
}
// Support simple template substitution (e.g., "req-{timestamp}")
return template
.replace('{timestamp}', Date.now())
.replace('{uuid}', generateUUID());
}
/**
* Send query to RAG backend using current environment settings (Bench Mode)
*/
async function sendQuery(requestBody) {
const url = buildApiUrl();
const requestId = generateRequestId();
const envSettings = getCurrentEnvSettings();
const env = getCurrentEnv();
env.requestId = requestId;
env.requestTimestamp = new Date().toISOString();
const headers = {
'Content-Type': 'application/json',
'Request-Id': requestId,
'System-Id': envSettings.systemId,
};
// Add Bearer token if configured
if (envSettings.bearerToken) {
headers['Authorization'] = `Bearer ${envSettings.bearerToken}`;
}
// Add System-Platform header if configured
if (envSettings.systemPlatform) {
headers['System-Platform'] = envSettings.systemPlatform;
}
// Add System-Platform-User header if configured
if (envSettings.systemPlatformUser) {
headers['System-Platform-User'] = envSettings.systemPlatformUser;
}
try {
const response = await fetch(url, {
method: 'POST',
headers: headers,
body: JSON.stringify(requestBody),
signal: AbortSignal.timeout(AppState.settings.requestTimeout),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
return data;
} catch (error) {
if (error.name === 'AbortError') {
throw new Error('Запрос превысил таймаут (30 минут)');
}
throw error;
}
}
/**
* Build Backend API URL
*/
function buildBackendApiUrl(endpoint) {
const envSettings = getCurrentEnvSettings();
const { host, port, fullUrl } = envSettings;
if (fullUrl) {
// If fullUrl provided, use it as base
const baseUrl = fullUrl.replace(/\/$/, ''); // Remove trailing slash
return `${baseUrl}/${endpoint}`;
}
const portPart = port ? `:${port}` : '';
const endpointPart = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
return `https://${host}${portPart}${endpointPart}`;
}
/**
* Build headers for Backend API
*/
function buildBackendHeaders() {
const envSettings = getCurrentEnvSettings();
const headers = {
'Content-Type': 'application/json',
'Platform-User-Id': envSettings.platformUserId,
'Platform-Id': envSettings.platformId,
};
// Add Bearer token if configured
if (envSettings.bearerToken) {
headers['Authorization'] = `Bearer ${envSettings.bearerToken}`;
}
return headers;
}
/**
* Send single question to Backend /ask endpoint
*/
async function sendBackendAsk(question, userMessageId) {
const envSettings = getCurrentEnvSettings();
const url = buildBackendApiUrl(envSettings.backendAskEndpoint);
const headers = buildBackendHeaders();
const requestBody = {
question: question,
user_message_id: userMessageId,
user_message_datetime: new Date().toISOString(),
with_classify: envSettings.withClassify || false,
};
try {
const response = await fetch(url, {
method: 'POST',
headers: headers,
body: JSON.stringify(requestBody),
signal: AbortSignal.timeout(AppState.settings.requestTimeout),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
return data; // Returns AskRagResponse: { answers: [], metadata: {}, question_type: }
} catch (error) {
if (error.name === 'AbortError') {
throw new Error('Запрос превысил таймаут');
}
throw error;
}
}
/**
* Reset session via Backend /context/reset-session endpoint
*/
async function sendBackendResetSession() {
const envSettings = getCurrentEnvSettings();
const url = buildBackendApiUrl(envSettings.backendResetEndpoint);
const headers = buildBackendHeaders();
const requestBody = {
user_message_datetime: new Date().toISOString(),
};
try {
const response = await fetch(url, {
method: 'POST',
headers: headers,
body: JSON.stringify(requestBody),
signal: AbortSignal.timeout(AppState.settings.requestTimeout),
});
// Reset returns 204 No Content on success
if (response.status === 204 || response.ok) {
return true;
}
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
} catch (error) {
if (error.name === 'AbortError') {
throw new Error('Запрос превысил таймаут');
}
throw error;
}
}
/**
* Send queries in Backend mode (one by one)
*/
async function sendBackendQuery(questions) {
const envSettings = getCurrentEnvSettings();
const env = getCurrentEnv();
const resetSession = envSettings.resetSessionMode !== false;
env.requestId = generateUUID();
env.requestTimestamp = new Date().toISOString();
const results = [];
let userMessageId = 1;
for (let i = 0; i < questions.length; i++) {
const question = questions[i];
// Update loading message with progress
updateLoadingMessage(`Вопрос ${i + 1} из ${questions.length}: отправка...`);
try {
// Send question
const response = await sendBackendAsk(question, userMessageId);
// Store result
results.push({
question: question,
answers: response.answers || [],
question_type: response.question_type || null,
metadata: response.metadata || {},
user_message_id: userMessageId,
});
userMessageId++;
// Reset session if needed
if (resetSession && i < questions.length - 1) {
updateLoadingMessage(`Вопрос ${i + 1} из ${questions.length}: сброс сессии...`);
await sendBackendResetSession();
}
} catch (error) {
// Store error for this question
results.push({
question: question,
answers: [`Ошибка: ${error.message}`],
question_type: null,
metadata: {},
user_message_id: userMessageId,
error: true,
});
userMessageId++;
}
}
// Convert to RagResponseBenchList-like format for compatibility with existing UI
return {
answers: results.map(r => ({
question: r.question,
body_research: r.answers.join('\n\n'),
body_analytical_hub: r.question_type ? `Тип вопроса: ${r.question_type}` : '',
processing_time_sec: 0, // Backend doesn't provide this
docs_from_vectorstore: null,
docs_to_llm: null,
metadata: r.metadata,
backend_mode: true,
}))
};
}
/**
* Update loading message
*/
function updateLoadingMessage(message) {
const loadingMessageEl = document.getElementById('loading-message');
if (loadingMessageEl) {
loadingMessageEl.textContent = message;
}
}
// ============================================
// UI Initialization
// ============================================
/**
* Initialize application
*/
function initApp() {
// Load settings
AppState.settings = loadSettings();
AppState.currentEnvironment = AppState.settings.activeEnvironment || '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() {
// 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);
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;
saveSettings(AppState.settings);
// 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');
}
function saveSettingsHandler() {
const settings = readSettingsFromDialog();
saveSettings(settings);
closeSettingsDialog();
showToast('Настройки сохранены', 'success');
}
function resetSettings() {
if (confirm('Сбросить все настройки к значениям по умолчанию?')) {
AppState.settings = { ...defaultSettings };
saveSettings(AppState.settings);
populateSettingsDialog();
showToast('Настройки сброшены', 'info');
}
}
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 localStorage
saveSettings(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';
let response;
let questions;
if (apiMode === 'backend') {
// Backend mode: extract questions as string array
questions = extractQuestions();
env.currentRequest = questions.map(q => ({ body: q, backend_mode: true }));
// Show loading
showLoading('Отправка вопросов к Backend API...');
// Send queries one by one
response = await sendBackendQuery(questions);
} else {
// Bench mode: use original logic
const requestBody = buildRequestBody();
env.currentRequest = requestBody;
// Show loading
showLoading('Отправка запроса к RAG бэкенду...');
// Send query
response = await sendQuery(requestBody);
}
// Hide loading
hideLoading();
// Validate response
if (!response.answers || !Array.isArray(response.answers)) {
throw new Error('Некорректный формат ответа: отсутствует поле "answers"');
}
// Save response
env.currentResponse = response;
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}] Получено ${response.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;
saveSettings(AppState.settings);
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 = `
<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>
`;
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 = '<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="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('');
}
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 = '<p class="color-warning">Нет данных</p>';
return;
}
if (isTableText(text)) {
const table = parseTextTable(text);
if (table) {
container.innerHTML = `<div class="table-container">${table}</div>`;
return;
}
}
// Render as plain text with line breaks
container.innerHTML = `<p>${escapeHtml(text).replace(/\n/g, '<br>')}</p>`;
}
function renderDocuments(containerId, docs, section, subsection, answerIndex) {
const container = document.getElementById(containerId);
if (!docs || docs.length === 0) {
container.innerHTML = `
<div class="empty-state">
<div class="empty-state-text">Нет документов</div>
</div>
`;
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 || `<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="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 class="form-label">Комментарий</label>
<textarea class="form-textarea" rows="2"
data-section="${section}"
data-subsection="${subsection}"
data-doc-index="${docIndex}"
placeholder="Комментарий к документу..."></textarea>
</div>
</div>
</div>
</div>
</div>
`;
}).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
// ============================================
document.addEventListener('DOMContentLoaded', initApp);