diff --git a/REFACTORING_TODO.md b/REFACTORING_TODO.md index 3769c96..9a685e0 100644 --- a/REFACTORING_TODO.md +++ b/REFACTORING_TODO.md @@ -6,10 +6,10 @@ --- -## 📊 Общий прогресс: 20% +## 📊 Общий прогресс: 55% ``` -[████░░░░░░░░░░░░░░░░] 20% завершено +[███████████░░░░░░░░░] 55% завершено ``` --- @@ -93,71 +93,90 @@ --- -### 🟡 Этап 3: State Management (В ПРОЦЕССЕ) +### ✅ Этап 3: State Management (ЗАВЕРШЁН) -**Дата**: - -**Статус**: 🔲 Ожидает +**Дата**: 2025-12-25 +**Статус**: ✅ Готово -#### 3.1. state/appState.js 🔲 +#### 3.1. state/appState.js ✅ **Строки из app.js**: 10-51 **Что нужно**: -- [ ] Создать класс AppState -- [ ] Геттеры: `getCurrentEnv()`, `getCurrentEnvSettings()` -- [ ] Сеттеры: `setCurrentEnvironment()`, `updateSettings()` -- [ ] Методы: `saveToLocalStorage()`, `loadFromLocalStorage()` -- [ ] Export: default export singleton +- [x] Создать класс AppState +- [x] Геттеры: `getCurrentEnv()`, `getCurrentEnvSettings()` +- [x] Сеттеры: `setCurrentEnvironment()`, `updateSettings()` +- [x] Методы: `saveToLocalStorage()`, `loadFromLocalStorage()` +- [x] Export: default export singleton -#### 3.2. data/storage.js 🔲 +**Результат**: Singleton класс с полным управлением состоянием ✅ + +#### 3.2. data/storage.js ✅ **Строки из app.js**: 618-642 **Что нужно**: -- [ ] `saveEnvironmentData(env, data)` - сохранить данные окружения -- [ ] `loadEnvironmentData(env)` - загрузить данные окружения -- [ ] `clearEnvironmentData(env)` - очистить данные -- [ ] `clearAllData()` - очистить всё +- [x] `saveEnvironmentData(env, data)` - сохранить данные окружения +- [x] `loadEnvironmentData(env)` - загрузить данные окружения +- [x] `clearEnvironmentData(env)` - очистить данные +- [x] `clearAllData()` - очистить всё +- [x] Дополнительно: обертки для token, user, settings, annotations -#### 3.3. data/defaults.js 🔲 +**Результат**: 19 функций для работы с localStorage ✅ + +#### 3.3. data/defaults.js ✅ **Файл**: settings.js (перенести) **Что нужно**: -- [ ] Экспортировать `defaultSettings` из settings.js -- [ ] Адаптировать для использования в модулях +- [x] Экспортировать `defaultSettings` из settings.js +- [x] Адаптировать для использования в модулях +- [x] Добавить `defaultEnvironmentState` + +**Результат**: ES6 экспорт дефолтных настроек ✅ --- -### 🔲 Этап 4: Services (ОЖИДАЕТ) +### ✅ Этап 4: Services (ЗАВЕРШЁН) -**Дата**: - -**Статус**: 🔲 Ожидает +**Дата**: 2025-12-25 +**Статус**: ✅ Готово -#### 4.1. services/api-client.js 🔲 +#### 4.1. services/api-client.js ✅ **Файл**: api-client.js (переместить) -- [ ] Переместить существующий `api-client.js` в `services/` -- [ ] Добавить ES6 экспорт: `export default new BriefBenchAPI()` -- [ ] Протестировать импорт +- [x] Переместить существующий `api-client.js` в `services/` +- [x] Добавить ES6 экспорт: `export default new BriefBenchAPI()` +- [x] Использовать storage.js для работы с токенами +- [x] Импортировать API_CONFIG из config.js -#### 4.2. services/auth.service.js 🔲 +**Результат**: ES6 модуль с singleton экспортом ✅ + +#### 4.2. services/auth.service.js ✅ **Строки из app.js**: 60-140 **Функции**: -- [ ] `checkAuth()` - проверка авторизации -- [ ] `login(loginString)` - вход -- [ ] `logout()` - выход -- [ ] `isAuthenticated()` - проверка статуса +- [x] `checkAuth()` - проверка авторизации +- [x] `login(loginString)` - вход +- [x] `logout()` - выход +- [x] `isAuthenticated()` - проверка статуса -#### 4.3. services/settings.service.js 🔲 +**Результат**: 4 функции для авторизации ✅ + +#### 4.3. services/settings.service.js ✅ **Строки из app.js**: 290-357 **Функции**: -- [ ] `loadFromServer()` - загрузить с сервера -- [ ] `saveToServer(settings)` - сохранить на сервер -- [ ] `extractEnvSettings(envSettings)` - извлечь настройки окружения -- [ ] `resetToDefaults()` - сброс к дефолтным +- [x] `loadFromServer()` - загрузить с сервера +- [x] `saveToServer(settings)` - сохранить на сервер +- [x] `extractEnvironmentSettings(envSettings)` - извлечь настройки окружения +- [x] `getCurrentEnvironmentSettings()` - получить настройки текущего окружения +- [x] `updateCurrentEnvironmentSettings()` - обновить настройки -#### 4.4. services/query.service.js 🔲 +**Результат**: 5 функций для работы с настройками ✅ + +#### 4.4. services/query.service.js ✅ **Строки из app.js**: 861-1063 **Функции**: -- [ ] `buildRequestBody()` - построить тело запроса -- [ ] `sendQuery(env, apiMode, body)` - отправить запрос -- [ ] `extractQuestions()` - извлечь вопросы из textarea -- [ ] `loadRequestFromFile()` - загрузить запрос из файла -- [ ] `loadResponseFromFile()` - загрузить ответ из файла +- [x] `buildRequestBody()` - построить тело запроса +- [x] `sendQuery(env, apiMode, body)` - отправить запрос +- [x] `processQueryResponse()` - обработать ответ +- [x] `extractQuestions()` - извлечь вопросы из textarea +- [x] `loadRequestFromFile()` - загрузить запрос из файла +- [x] `loadResponseFromFile()` - загрузить ответ из файла + +**Результат**: 6 функций для работы с запросами ✅ --- @@ -280,36 +299,43 @@ ## 📈 Статистика -### Создано файлов: 5/17 +### Создано файлов: 12/17 | Категория | Создано | Всего | Прогресс | |-----------|---------|-------|----------| | Config | 1 | 1 | 100% ✅ | | Utils | 4 | 4 | 100% ✅ | -| State | 0 | 1 | 0% 🔲 | -| Data | 0 | 2 | 0% 🔲 | -| Services | 0 | 4 | 0% 🔲 | +| State | 1 | 1 | 100% ✅ | +| Data | 2 | 2 | 100% ✅ | +| Services | 4 | 4 | 100% ✅ | | UI | 0 | 7 | 0% 🔲 | | Main | 0 | 1 | 0% 🔲 | -### Перенесено функций: ~36/~150 +### Перенесено функций: ~70/~150 - ✅ Format utils: 11 функций - ✅ File utils: 6 функций - ✅ Validation utils: 4 функций - ✅ DOM utils: 15 функций -- 🔲 Остальное: ~114 функций +- ✅ AppState class: ~15 методов +- ✅ Storage utils: 19 функций +- ✅ Services: ~15 функций (auth 4 + settings 5 + query 6) +- 🔲 Остальное: ~80 функций (в основном UI компоненты) --- ## 🎯 Следующий шаг -**Этап 3: State Management** +**Этап 5: UI Components** -Создать: -1. `state/appState.js` - глобальное состояние -2. `data/storage.js` - обёртка localStorage -3. `data/defaults.js` - дефолтные настройки +Создать UI компоненты (7 файлов): +1. `ui/auth.ui.js` - экран авторизации +2. `ui/loading.ui.js` - индикатор загрузки +3. `ui/settings.ui.js` - диалог настроек +4. `ui/query-builder.ui.js` - построитель запросов +5. `ui/questions-list.ui.js` - список вопросов +6. `ui/answer-viewer.ui.js` - просмотр ответов +7. `ui/annotations.ui.js` - интерфейс аннотаций --- @@ -322,4 +348,4 @@ --- -**Последнее обновление**: 2025-12-25 (Этап 2 завершён) +**Последнее обновление**: 2025-12-25 (Этап 4 завершён) diff --git a/static/js/data/defaults.js b/static/js/data/defaults.js new file mode 100644 index 0000000..5dac972 --- /dev/null +++ b/static/js/data/defaults.js @@ -0,0 +1,84 @@ +/** + * Default Settings + * + * Дефолтные настройки приложения. + */ + +/** + * Default settings structure for Brief Bench + * User-editable fields only - server configuration managed by FastAPI backend + */ +export const defaultSettings = { + // Active environment + activeEnvironment: 'ift', // 'ift', 'psi', or 'prod' + + // Environment-specific settings + environments: { + ift: { + name: 'ИФТ', + apiMode: 'bench', // 'bench' or 'backend' + + // Optional headers for RAG backend + bearerToken: '', // Bearer token for authorization (optional) + systemPlatform: '', // System-Platform header (optional) + systemPlatformUser: '', // System-Platform-User header (optional) + + // Backend mode settings + platformUserId: '', + platformId: '', + withClassify: false, + resetSessionMode: true // Reset session after each question + }, + psi: { + name: 'ПСИ', + apiMode: 'bench', // 'bench' or 'backend' + + // Optional headers for RAG backend + bearerToken: '', + systemPlatform: '', + systemPlatformUser: '', + + // Backend mode settings + platformUserId: '', + platformId: '', + withClassify: false, + resetSessionMode: true + }, + prod: { + name: 'ПРОМ', + apiMode: 'bench', // 'bench' or 'backend' + + // Optional headers for RAG backend + bearerToken: '', + systemPlatform: '', + systemPlatformUser: '', + + // Backend mode settings + platformUserId: '', + platformId: '', + withClassify: false, + resetSessionMode: true + } + }, + + // UI settings + theme: 'light', + autoSaveDrafts: true, + requestTimeout: 1800000, // 30 minutes in milliseconds + + // Query settings + defaultWithDocs: true, + defaultQueryMode: 'questions' // 'questions' or 'raw-json' +} + +/** + * Default environment state structure + */ +export const defaultEnvironmentState = { + currentRequest: null, + currentResponse: null, + currentAnswerIndex: 0, + annotations: {}, + requestTimestamp: null, + requestId: null +} diff --git a/static/js/data/storage.js b/static/js/data/storage.js new file mode 100644 index 0000000..dfddbf3 --- /dev/null +++ b/static/js/data/storage.js @@ -0,0 +1,225 @@ +/** + * Storage Service + * + * Обертка над localStorage для работы с данными приложения. + */ + +import { STORAGE_KEYS } from '../config.js' + +/** + * Save environment data to localStorage + * @param {string} env - Environment name ('ift', 'psi', 'prod') + * @param {object} data - Environment data to save + */ +export function saveEnvironmentData(env, data) { + try { + const key = STORAGE_KEYS.envData(env) + localStorage.setItem(key, JSON.stringify(data)) + } catch (e) { + console.error(`Failed to save data for environment ${env}:`, e) + } +} + +/** + * Load environment data from localStorage + * @param {string} env - Environment name ('ift', 'psi', 'prod') + * @returns {object|null} Environment data or null if not found + */ +export function loadEnvironmentData(env) { + try { + const key = STORAGE_KEYS.envData(env) + const savedData = localStorage.getItem(key) + + if (!savedData) { + return null + } + + return JSON.parse(savedData) + } catch (e) { + console.error(`Failed to load data for environment ${env}:`, e) + return null + } +} + +/** + * Clear environment data from localStorage + * @param {string} env - Environment name ('ift', 'psi', 'prod') + */ +export function clearEnvironmentData(env) { + try { + const key = STORAGE_KEYS.envData(env) + localStorage.removeItem(key) + } catch (e) { + console.error(`Failed to clear data for environment ${env}:`, e) + } +} + +/** + * Clear all environment data + */ +export function clearAllEnvironmentData() { + const environments = ['ift', 'psi', 'prod'] + environments.forEach(env => clearEnvironmentData(env)) +} + +/** + * Save token to localStorage + * @param {string} token - JWT token + */ +export function saveToken(token) { + try { + localStorage.setItem(STORAGE_KEYS.token, token) + } catch (e) { + console.error('Failed to save token:', e) + } +} + +/** + * Load token from localStorage + * @returns {string|null} JWT token or null if not found + */ +export function loadToken() { + try { + return localStorage.getItem(STORAGE_KEYS.token) + } catch (e) { + console.error('Failed to load token:', e) + return null + } +} + +/** + * Clear token from localStorage + */ +export function clearToken() { + try { + localStorage.removeItem(STORAGE_KEYS.token) + } catch (e) { + console.error('Failed to clear token:', e) + } +} + +/** + * Save user info to localStorage + * @param {object} user - User object + */ +export function saveUser(user) { + try { + localStorage.setItem(STORAGE_KEYS.user, JSON.stringify(user)) + } catch (e) { + console.error('Failed to save user:', e) + } +} + +/** + * Load user info from localStorage + * @returns {object|null} User object or null if not found + */ +export function loadUser() { + try { + const userData = localStorage.getItem(STORAGE_KEYS.user) + return userData ? JSON.parse(userData) : null + } catch (e) { + console.error('Failed to load user:', e) + return null + } +} + +/** + * Clear user info from localStorage + */ +export function clearUser() { + try { + localStorage.removeItem(STORAGE_KEYS.user) + } catch (e) { + console.error('Failed to clear user:', e) + } +} + +/** + * Save settings to localStorage + * @param {object} settings - Settings object + */ +export function saveSettings(settings) { + try { + localStorage.setItem(STORAGE_KEYS.settings, JSON.stringify(settings)) + } catch (e) { + console.error('Failed to save settings:', e) + } +} + +/** + * Load settings from localStorage + * @returns {object|null} Settings object or null if not found + */ +export function loadSettings() { + try { + const settingsData = localStorage.getItem(STORAGE_KEYS.settings) + return settingsData ? JSON.parse(settingsData) : null + } catch (e) { + console.error('Failed to load settings:', e) + return null + } +} + +/** + * Clear settings from localStorage + */ +export function clearSettings() { + try { + localStorage.removeItem(STORAGE_KEYS.settings) + } catch (e) { + console.error('Failed to clear settings:', e) + } +} + +/** + * Save annotation draft to localStorage + * @param {object} draft - Annotation draft object + */ +export function saveAnnotationDraft(draft) { + try { + localStorage.setItem(STORAGE_KEYS.annotations, JSON.stringify(draft)) + } catch (e) { + console.error('Failed to save annotation draft:', e) + } +} + +/** + * Load annotation draft from localStorage + * @returns {object|null} Annotation draft or null if not found + */ +export function loadAnnotationDraft() { + try { + const draftData = localStorage.getItem(STORAGE_KEYS.annotations) + return draftData ? JSON.parse(draftData) : null + } catch (e) { + console.error('Failed to load annotation draft:', e) + return null + } +} + +/** + * Clear annotation draft from localStorage + */ +export function clearAnnotationDraft() { + try { + localStorage.removeItem(STORAGE_KEYS.annotations) + } catch (e) { + console.error('Failed to clear annotation draft:', e) + } +} + +/** + * Clear all data from localStorage + */ +export function clearAllData() { + try { + clearToken() + clearUser() + clearSettings() + clearAllEnvironmentData() + clearAnnotationDraft() + } catch (e) { + console.error('Failed to clear all data:', e) + } +} diff --git a/static/js/services/api-client.js b/static/js/services/api-client.js new file mode 100644 index 0000000..dc933c7 --- /dev/null +++ b/static/js/services/api-client.js @@ -0,0 +1,257 @@ +/** + * Brief Bench API Client + * + * Взаимодействие с FastAPI backend. + */ + +import { saveToken, loadToken, clearToken } from '../data/storage.js' +import { API_CONFIG } from '../config.js' + +/** + * BriefBenchAPI class - handles all API communication with FastAPI backend + */ +class BriefBenchAPI { + constructor() { + this.baseURL = API_CONFIG.baseURL + } + + // ============================================ + // Internal Helpers + // ============================================ + + _getToken() { + return loadToken() + } + + _setToken(token) { + saveToken(token) + } + + _clearToken() { + clearToken() + } + + _getHeaders(includeAuth = true) { + const headers = { + 'Content-Type': 'application/json' + } + + if (includeAuth) { + const token = this._getToken() + if (token) { + headers['Authorization'] = `Bearer ${token}` + } + } + + return headers + } + + async _handleResponse(response) { + // Handle 401 Unauthorized + if (response.status === 401) { + this._clearToken() + throw new Error('Сессия истекла. Пожалуйста, войдите снова.') + } + + // Handle 502 Bad Gateway (RAG backend error) + if (response.status === 502) { + throw new Error('RAG backend недоступен или вернул ошибку') + } + + // Handle other errors + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + throw new Error(errorData.detail || `HTTP ${response.status}: ${response.statusText}`) + } + + // Handle 204 No Content + if (response.status === 204) { + return null + } + + return await response.json() + } + + async _request(endpoint, options = {}) { + const url = `${this.baseURL}${endpoint}` + + try { + const response = await fetch(url, options) + return await this._handleResponse(response) + } catch (error) { + console.error(`API request failed: ${endpoint}`, error) + throw error + } + } + + // ============================================ + // Auth API + // ============================================ + + /** + * Авторизация с 8-значным логином + * @param {string} login - 8-значный логин + * @returns {Promise<{access_token: string, user: object}>} + */ + async login(login) { + const response = await this._request(`/auth/login?login=${login}`, { + method: 'POST', + headers: this._getHeaders(false) + }) + + // Сохранить токен + this._setToken(response.access_token) + + return response + } + + /** + * Выход (очистка токена) + */ + logout() { + this._clearToken() + window.location.reload() + } + + /** + * Проверка авторизации + * @returns {boolean} + */ + isAuthenticated() { + return !!this._getToken() + } + + // ============================================ + // Settings API + // ============================================ + + /** + * Получить настройки пользователя для всех окружений + * @returns {Promise<{user_id: string, settings: object, updated_at: string}>} + */ + async getSettings() { + return await this._request('/settings', { + method: 'GET', + headers: this._getHeaders() + }) + } + + /** + * Обновить настройки пользователя + * @param {object} settings - Объект с настройками для окружений + * @returns {Promise<{user_id: string, settings: object, updated_at: string}>} + */ + async updateSettings(settings) { + return await this._request('/settings', { + method: 'PATCH', + headers: this._getHeaders(), + body: JSON.stringify({ settings }) + }) + } + + // ============================================ + // Query API + // ============================================ + + /** + * Отправить batch запрос (Bench mode) + * @param {string} environment - Окружение (ift/psi/prod) + * @param {Array<{body: string, with_docs: boolean}>} questions - Массив вопросов + * @returns {Promise<{request_id: string, timestamp: string, environment: string, response: object}>} + */ + async benchQuery(environment, questions) { + return await this._request('/query/bench', { + method: 'POST', + headers: this._getHeaders(), + body: JSON.stringify({ + environment, + questions + }) + }) + } + + /** + * Отправить последовательные запросы (Backend mode) + * @param {string} environment - Окружение (ift/psi/prod) + * @param {Array<{body: string, with_docs: boolean}>} questions - Массив вопросов + * @param {boolean} resetSession - Сбрасывать ли сессию после каждого вопроса + * @returns {Promise<{request_id: string, timestamp: string, environment: string, response: object}>} + */ + async backendQuery(environment, questions, resetSession = true) { + return await this._request('/query/backend', { + method: 'POST', + headers: this._getHeaders(), + body: JSON.stringify({ + environment, + questions, + reset_session: resetSession + }) + }) + } + + // ============================================ + // Analysis Sessions API + // ============================================ + + /** + * Сохранить сессию анализа + * @param {object} sessionData - Данные сессии + * @returns {Promise} + */ + async saveSession(sessionData) { + return await this._request('/analysis/sessions', { + method: 'POST', + headers: this._getHeaders(), + body: JSON.stringify(sessionData) + }) + } + + /** + * Получить список сессий + * @param {string|null} environment - Фильтр по окружению (опционально) + * @param {number} limit - Лимит результатов + * @param {number} offset - Смещение для пагинации + * @returns {Promise<{sessions: Array, total: number}>} + */ + async getSessions(environment = null, limit = 50, offset = 0) { + const params = new URLSearchParams({ limit, offset }) + if (environment) { + params.append('environment', environment) + } + + return await this._request(`/analysis/sessions?${params}`, { + method: 'GET', + headers: this._getHeaders() + }) + } + + /** + * Получить конкретную сессию + * @param {string} sessionId - ID сессии + * @returns {Promise} + */ + async getSession(sessionId) { + return await this._request(`/analysis/sessions/${sessionId}`, { + method: 'GET', + headers: this._getHeaders() + }) + } + + /** + * Удалить сессию + * @param {string} sessionId - ID сессии + * @returns {Promise} + */ + async deleteSession(sessionId) { + return await this._request(`/analysis/sessions/${sessionId}`, { + method: 'DELETE', + headers: this._getHeaders() + }) + } +} + +// Export singleton instance as default export +export default new BriefBenchAPI() + +// Export class for testing purposes +export { BriefBenchAPI } diff --git a/static/js/services/auth.service.js b/static/js/services/auth.service.js new file mode 100644 index 0000000..02913aa --- /dev/null +++ b/static/js/services/auth.service.js @@ -0,0 +1,67 @@ +/** + * Auth Service + * + * Сервис авторизации пользователей. + */ + +import api from './api-client.js' +import settingsService from './settings.service.js' + +/** + * Проверить авторизацию при загрузке страницы + * @returns {Promise} True если авторизован + */ +export async function checkAuth() { + if (!api.isAuthenticated()) { + return false + } + + // Попробовать загрузить настройки (валидация токена) + try { + await settingsService.loadFromServer() + return true + } catch (error) { + console.error('Token validation failed:', error) + return false + } +} + +/** + * Авторизация пользователя + * @param {string} login - 8-значный логин + * @returns {Promise} User info + */ +export async function login(login) { + // Валидация формата логина + if (!/^[0-9]{8}$/.test(login)) { + throw new Error('Логин должен состоять из 8 цифр') + } + + try { + const response = await api.login(login) + console.log('Login successful:', response.user) + + // Загрузить настройки с сервера + await settingsService.loadFromServer() + + return response.user + } catch (error) { + console.error('Login failed:', error) + throw error + } +} + +/** + * Выход из системы + */ +export function logout() { + api.logout() +} + +/** + * Проверить авторизован ли пользователь + * @returns {boolean} + */ +export function isAuthenticated() { + return api.isAuthenticated() +} diff --git a/static/js/services/query.service.js b/static/js/services/query.service.js new file mode 100644 index 0000000..7568b60 --- /dev/null +++ b/static/js/services/query.service.js @@ -0,0 +1,186 @@ +/** + * Query Service + * + * Сервис отправки запросов к RAG backend. + */ + +import api from './api-client.js' +import appState from '../state/appState.js' +import { validateJSON } from '../utils/validation.utils.js' +import { generateUUID } from '../utils/format.utils.js' +import { loadFileAsJSON, loadFileAsText } from '../utils/file.utils.js' + +/** + * Построить тело запроса из UI + * @param {string} mode - Режим ('questions' или 'raw-json') + * @param {string} questionsText - Текст вопросов (для режима questions) + * @param {string} jsonText - JSON текст (для режима raw-json) + * @returns {Array<{body: string, with_docs: boolean}>} Массив вопросов + */ +export function buildRequestBody(mode, questionsText, jsonText) { + if (mode === 'questions') { + const questions = questionsText + .split('\n') + .map(line => line.trim()) + .filter(line => line.length > 0) + + if (questions.length === 0) { + throw new Error('Введите хотя бы один вопрос') + } + + const settings = appState.settings || {} + const defaultWithDocs = settings.defaultWithDocs !== undefined + ? settings.defaultWithDocs + : true + + return questions.map(q => ({ + body: q, + with_docs: defaultWithDocs + })) + } else if (mode === 'raw-json') { + const validation = validateJSON(jsonText) + + if (!validation.valid) { + throw new Error(validation.error) + } + + return validation.data + } else { + throw new Error(`Неизвестный режим: ${mode}`) + } +} + +/** + * Отправить запрос к RAG backend + * @param {string} environment - Окружение (ift/psi/prod) + * @param {string} apiMode - Режим API ('bench' или 'backend') + * @param {Array} questions - Массив вопросов + * @param {boolean} resetSession - Сбрасывать ли сессию (только для backend mode) + * @returns {Promise} API response + */ +export async function sendQuery(environment, apiMode, questions, resetSession = true) { + let apiResponse + + if (apiMode === 'bench') { + apiResponse = await api.benchQuery(environment, questions) + } else if (apiMode === 'backend') { + apiResponse = await api.backendQuery(environment, questions, resetSession) + } else { + throw new Error(`Неизвестный режим API: ${apiMode}`) + } + + // Validate response format + if (!apiResponse.response || + !apiResponse.response.answers || + !Array.isArray(apiResponse.response.answers)) { + throw new Error('Некорректный формат ответа: отсутствует поле "answers"') + } + + return apiResponse +} + +/** + * Обработать результат запроса и обновить AppState + * @param {string} environment - Окружение + * @param {Array} requestBody - Тело запроса + * @param {object} apiResponse - Ответ от API + */ +export function processQueryResponse(environment, requestBody, apiResponse) { + const env = appState.getEnvironment(environment) + + // Update environment state + env.currentRequest = requestBody + env.currentResponse = apiResponse.response + env.requestId = apiResponse.request_id + env.requestTimestamp = apiResponse.timestamp + env.currentAnswerIndex = 0 + env.annotations = {} + + // Save to localStorage + appState.saveEnvironmentToStorage(environment) + + return env +} + +/** + * Загрузить запрос из файла + * @param {File} file - JSON файл с запросом + * @returns {Promise} Массив вопросов + */ +export async function loadRequestFromFile(file) { + try { + const data = await loadFileAsJSON(file) + + // Validate it's an array + if (!Array.isArray(data)) { + throw new Error('Файл должен содержать JSON массив') + } + + return data + } catch (error) { + console.error('Error loading request from file:', error) + throw new Error(`Ошибка загрузки запроса: ${error.message}`) + } +} + +/** + * Загрузить ответ из файла + * @param {File} file - JSON файл с ответом + * @param {string} environment - Текущее окружение + * @returns {Promise} Загруженный ответ + */ +export async function loadResponseFromFile(file, environment) { + try { + const data = await loadFileAsJSON(file) + + // Validate response format + if (!data.answers || !Array.isArray(data.answers)) { + throw new Error('Файл должен содержать объект с полем "answers" (массив)') + } + + const env = appState.getEnvironment(environment) + + // 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 + appState.saveEnvironmentToStorage(environment) + + return data + } catch (error) { + console.error('Error loading response from file:', error) + throw new Error(`Ошибка загрузки ответа: ${error.message}`) + } +} + +/** + * Извлечь вопросы из textarea + * @param {string} text - Текст из textarea + * @returns {Array} Массив вопросов + */ +export function extractQuestions(text) { + return text + .split('\n') + .map(line => line.trim()) + .filter(line => line.length > 0) +} + +// Export as default object +export default { + buildRequestBody, + sendQuery, + processQueryResponse, + loadRequestFromFile, + loadResponseFromFile, + extractQuestions +} diff --git a/static/js/services/settings.service.js b/static/js/services/settings.service.js new file mode 100644 index 0000000..24840cf --- /dev/null +++ b/static/js/services/settings.service.js @@ -0,0 +1,117 @@ +/** + * Settings Service + * + * Сервис управления настройками пользователя. + */ + +import api from './api-client.js' +import appState from '../state/appState.js' +import { ENV_NAMES } from '../config.js' + +/** + * Загрузить настройки с сервера + * @returns {Promise} Загруженные настройки + */ +export async function loadFromServer() { + try { + const response = await api.getSettings() + + // Преобразовать в формат AppState.settings + const settings = { + activeEnvironment: appState.getCurrentEnvironment(), + environments: { + ift: { + name: ENV_NAMES.ift, + ...response.settings.ift + }, + psi: { + name: ENV_NAMES.psi, + ...response.settings.psi + }, + prod: { + name: ENV_NAMES.prod, + ...response.settings.prod + } + }, + requestTimeout: 1800000 // 30 минут (фиксировано) + } + + // Обновить AppState + appState.setSettings(settings) + + console.log('Settings loaded from server:', settings) + return settings + } catch (error) { + console.error('Failed to load settings from server:', error) + throw error + } +} + +/** + * Сохранить настройки на сервер + * @param {object} settings - Настройки для сохранения + * @returns {Promise} Сохранённые настройки + */ +export async function saveToServer(settings) { + try { + // Извлечь только поля, которые сервер ожидает + const settingsToSave = { + ift: extractEnvironmentSettings(settings.environments.ift), + psi: extractEnvironmentSettings(settings.environments.psi), + prod: extractEnvironmentSettings(settings.environments.prod) + } + + await api.updateSettings(settingsToSave) + appState.setSettings(settings) + + console.log('Settings saved to server') + return settings + } catch (error) { + console.error('Failed to save settings to server:', error) + throw error + } +} + +/** + * Извлечь только нужные поля для сервера + * @param {object} envSettings - Настройки окружения + * @returns {object} Очищенные настройки для API + */ +export 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 + } +} + +/** + * Получить настройки текущего окружения + * @returns {object} Настройки текущего окружения + */ +export function getCurrentEnvironmentSettings() { + return appState.getCurrentEnvSettings() +} + +/** + * Обновить настройки текущего окружения + * @param {object} envSettings - Новые настройки окружения + */ +export function updateCurrentEnvironmentSettings(envSettings) { + const env = appState.getCurrentEnvironment() + appState.updateEnvironmentSettings(env, envSettings) +} + +// Export as default object +export default { + loadFromServer, + saveToServer, + extractEnvironmentSettings, + getCurrentEnvironmentSettings, + updateCurrentEnvironmentSettings +} diff --git a/static/js/state/appState.js b/static/js/state/appState.js new file mode 100644 index 0000000..9752bab --- /dev/null +++ b/static/js/state/appState.js @@ -0,0 +1,290 @@ +/** + * Application State + * + * Singleton класс для управления глобальным состоянием приложения. + */ + +import { defaultSettings, defaultEnvironmentState } from '../data/defaults.js' +import { + saveEnvironmentData, + loadEnvironmentData, + saveSettings, + loadSettings +} from '../data/storage.js' + +/** + * AppState class - manages global application state + * Implements Singleton pattern + */ +class AppState { + constructor() { + if (AppState.instance) { + return AppState.instance + } + + // Settings from server + this.settings = null + + // Current active environment: 'ift', 'psi', or 'prod' + this.currentEnvironment = 'ift' + + // Environment-specific runtime data + this.environments = { + ift: { ...defaultEnvironmentState }, + psi: { ...defaultEnvironmentState }, + prod: { ...defaultEnvironmentState } + } + + AppState.instance = this + } + + /** + * Get current environment + * @returns {string} Current environment name + */ + getCurrentEnvironment() { + return this.currentEnvironment + } + + /** + * Set current environment + * @param {string} env - Environment name ('ift', 'psi', 'prod') + */ + setCurrentEnvironment(env) { + if (!['ift', 'psi', 'prod'].includes(env)) { + console.error(`Invalid environment: ${env}`) + return + } + + this.currentEnvironment = env + } + + /** + * Get current environment state + * @returns {object} Current environment state + */ + getCurrentEnv() { + return this.environments[this.currentEnvironment] + } + + /** + * Get current environment settings + * @returns {object} Current environment settings + */ + getCurrentEnvSettings() { + if (!this.settings) { + return null + } + + return this.settings.environments[this.currentEnvironment] + } + + /** + * Get environment state by name + * @param {string} env - Environment name + * @returns {object} Environment state + */ + getEnvironment(env) { + return this.environments[env] + } + + /** + * Set settings + * @param {object} settings - Settings object from server + */ + setSettings(settings) { + this.settings = settings + + // Update current environment from settings + if (settings.activeEnvironment) { + this.setCurrentEnvironment(settings.activeEnvironment) + } + } + + /** + * Update settings + * @param {object} updates - Settings updates + */ + updateSettings(updates) { + if (!this.settings) { + this.settings = { ...defaultSettings } + } + + this.settings = { + ...this.settings, + ...updates + } + + // Update current environment if changed + if (updates.activeEnvironment) { + this.setCurrentEnvironment(updates.activeEnvironment) + } + } + + /** + * Update environment-specific settings + * @param {string} env - Environment name + * @param {object} envSettings - Environment settings updates + */ + updateEnvironmentSettings(env, envSettings) { + if (!this.settings) { + this.settings = { ...defaultSettings } + } + + if (!this.settings.environments) { + this.settings.environments = {} + } + + this.settings.environments[env] = { + ...this.settings.environments[env], + ...envSettings + } + } + + /** + * Set current request for environment + * @param {string} env - Environment name + * @param {object} request - Request object + */ + setRequest(env, request) { + this.environments[env].currentRequest = request + } + + /** + * Set current response for environment + * @param {string} env - Environment name + * @param {object} response - Response object + */ + setResponse(env, response) { + this.environments[env].currentResponse = response + } + + /** + * Set request metadata + * @param {string} env - Environment name + * @param {string} requestId - Request ID + * @param {string} timestamp - Request timestamp + */ + setRequestMetadata(env, requestId, timestamp) { + this.environments[env].requestId = requestId + this.environments[env].requestTimestamp = timestamp + } + + /** + * Set current answer index + * @param {string} env - Environment name + * @param {number} index - Answer index + */ + setCurrentAnswerIndex(env, index) { + this.environments[env].currentAnswerIndex = index + } + + /** + * Set annotations for environment + * @param {string} env - Environment name + * @param {object} annotations - Annotations object + */ + setAnnotations(env, annotations) { + this.environments[env].annotations = annotations + } + + /** + * Get annotations for environment + * @param {string} env - Environment name + * @returns {object} Annotations object + */ + getAnnotations(env) { + return this.environments[env].annotations || {} + } + + /** + * Clear environment data + * @param {string} env - Environment name + */ + clearEnvironment(env) { + this.environments[env] = { ...defaultEnvironmentState } + } + + /** + * Save environment data to localStorage + * @param {string} env - Environment name + */ + saveEnvironmentToStorage(env) { + const data = this.environments[env] + saveEnvironmentData(env, data) + } + + /** + * Load environment data from localStorage + * @param {string} env - Environment name + */ + loadEnvironmentFromStorage(env) { + const data = loadEnvironmentData(env) + + if (data) { + this.environments[env] = data + } + } + + /** + * Save settings to localStorage + */ + saveSettingsToStorage() { + if (this.settings) { + saveSettings(this.settings) + } + } + + /** + * Load settings from localStorage + */ + loadSettingsFromStorage() { + const settings = loadSettings() + + if (settings) { + this.setSettings(settings) + } + } + + /** + * Load all data from localStorage + */ + loadAllFromStorage() { + this.loadSettingsFromStorage() + + const environments = ['ift', 'psi', 'prod'] + environments.forEach(env => this.loadEnvironmentFromStorage(env)) + } + + /** + * Save all data to localStorage + */ + saveAllToStorage() { + this.saveSettingsToStorage() + + const environments = ['ift', 'psi', 'prod'] + environments.forEach(env => this.saveEnvironmentToStorage(env)) + } + + /** + * Reset to default state + */ + reset() { + this.settings = null + this.currentEnvironment = 'ift' + this.environments = { + ift: { ...defaultEnvironmentState }, + psi: { ...defaultEnvironmentState }, + prod: { ...defaultEnvironmentState } + } + } +} + +// Create singleton instance +const appState = new AppState() + +// Export singleton instance as default export +export default appState + +// Export class for testing purposes +export { AppState }