/** * 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 `${thead}${tbody}
`; } /** * 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 `
#${index + 1}
${ratingIndicator} ${annotationIndicator}
${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 `
Документ #${docIndex + 1} expand_more
${docContent}
Пометки
`; }).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() });