This commit is contained in:
itqop 2025-12-17 18:29:53 +03:00
parent 85dc167449
commit cf86a9378c
7 changed files with 5508 additions and 3 deletions

View File

@ -3,6 +3,7 @@
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from app.api.v1 import auth, settings as settings_router, query, analysis from app.api.v1 import auth, settings as settings_router, query, analysis
from app.config import settings from app.config import settings
@ -30,13 +31,19 @@ app.include_router(query.router, prefix="/api/v1")
app.include_router(analysis.router, prefix="/api/v1") app.include_router(analysis.router, prefix="/api/v1")
# Serve static files (frontend) # Serve static files (frontend)
# app.mount("/static", StaticFiles(directory="static"), name="static") app.mount("/static", StaticFiles(directory="static"), name="static")
@app.get("/app")
async def serve_frontend():
"""Serve the main frontend application."""
return FileResponse("static/index.html")
@app.get("/") @app.get("/")
async def root(): async def root():
"""Root endpoint.""" """Root endpoint - redirect to app."""
return {"message": "Brief Bench API", "version": "1.0.0"} return FileResponse("static/index.html")
@app.get("/health") @app.get("/health")

246
static/api-client.js Normal file
View File

@ -0,0 +1,246 @@
/**
* Brief Bench API Client
* Взаимодействие с FastAPI backend
*/
class BriefBenchAPI {
constructor() {
this.baseURL = '/api/v1'
}
// ============================================
// Internal Helpers
// ============================================
_getToken() {
return localStorage.getItem('access_token')
}
_setToken(token) {
localStorage.setItem('access_token', token)
}
_clearToken() {
localStorage.removeItem('access_token')
}
_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: 'PUT',
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<object>}
*/
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<object>}
*/
async getSession(sessionId) {
return await this._request(`/analysis/sessions/${sessionId}`, {
method: 'GET',
headers: this._getHeaders()
})
}
/**
* Удалить сессию
* @param {string} sessionId - ID сессии
* @returns {Promise<null>}
*/
async deleteSession(sessionId) {
return await this._request(`/analysis/sessions/${sessionId}`, {
method: 'DELETE',
headers: this._getHeaders()
})
}
}
// Export API client instance
const api = new BriefBenchAPI()

1671
static/app.js Normal file

File diff suppressed because it is too large Load Diff

1894
static/app.js.bak Normal file

File diff suppressed because it is too large Load Diff

451
static/index.html Normal file
View File

@ -0,0 +1,451 @@
<!DOCTYPE html>
<html lang="ru">
<!-- LOCAL -->
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Brief Bench - RAG Testing Interface</title>
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&family=Roboto+Mono:wght@400;500&display=swap" rel="stylesheet">
<!-- Material Icons -->
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<!-- Styles -->
<link rel="stylesheet" href="styles.css">
</head>
<body>
<!-- Login Screen -->
<div id="login-screen" class="login-container" style="display: none;">
<div class="login-card">
<h2 style="text-align: center; margin-bottom: var(--md-spacing-xl);">Brief Bench</h2>
<div class="form-group">
<label class="form-label" for="login-input">8-значный логин</label>
<input
type="text"
class="form-input"
id="login-input"
placeholder="12345678"
maxlength="8"
pattern="[0-9]{8}"
autocomplete="off"
>
<div class="form-helper-text" id="login-error" style="color: var(--md-error); display: none;"></div>
</div>
<button class="btn btn-filled" id="login-submit-btn" style="width: 100%;">
Войти
</button>
</div>
</div>
<div id="app">
<!-- App Bar -->
<header class="app-bar">
<div class="app-bar-title">
<button class="btn-icon" id="menu-btn" title="Toggle menu">
<span class="material-icons">menu</span>
</button>
<span>Brief Bench</span>
</div>
<div class="app-bar-actions">
<button class="btn-icon" id="new-query-btn" title="New Query">
<span class="material-icons">add_circle</span>
</button>
<button class="btn-icon" id="import-btn" title="Импорт анализа">
<span class="material-icons">folder_open</span>
</button>
<button class="btn-icon" id="export-btn" title="Экспорт анализа">
<span class="material-icons">save</span>
</button>
<button class="btn-icon" id="settings-btn" title="Settings">
<span class="material-icons">settings</span>
</button>
<button class="btn-icon" id="logout-btn" title="Выход">
<span class="material-icons">logout</span>
</button>
</div>
</header>
<!-- Environment Tabs -->
<div class="environment-tabs">
<button class="env-tab active" data-env="ift">ИФТ</button>
<button class="env-tab" data-env="psi">ПСИ</button>
<button class="env-tab" data-env="prod">ПРОМ</button>
</div>
<!-- Main Content Area -->
<div class="app-content">
<!-- Drawer / Sidebar -->
<aside class="drawer" id="drawer">
<div class="drawer-header">
<div style="flex: 1;">
<h6 style="margin-bottom: 4px;">Вопросы и ответы</h6>
<div class="text-caption" id="questions-count">0 вопросов</div>
</div>
<button class="btn-icon" id="clear-all-btn" title="Очистить всё (обновить страницу)" style="align-self: flex-start;">
<span class="material-icons">refresh</span>
</button>
</div>
<div class="drawer-content" id="questions-list">
<!-- Questions will be populated here -->
<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>
</div>
</aside>
<!-- Main Area -->
<main class="main-area" id="main-area">
<!-- Query Builder (shown when no data) -->
<div id="query-builder" class="hidden">
<div class="card">
<div class="card-header">
<h5 class="card-title">Создание запроса</h5>
<div class="toggle-group">
<button class="toggle-option active" data-mode="questions">Вопросы</button>
<button class="toggle-option" data-mode="raw-json">Raw JSON</button>
</div>
</div>
<div class="card-content">
<!-- Questions Mode -->
<div id="questions-mode" class="query-mode">
<div class="form-group">
<label class="form-label">Введите вопросы (каждый с новой строки)</label>
<textarea
id="questions-textarea"
class="form-textarea"
rows="10"
placeholder="Какая текущая ВВП в России?&#10;Какие показатели у газпрома"></textarea>
<div class="form-helper-text">Каждая строка будет отправлена как отдельный вопрос с флагом with_docs: true</div>
</div>
</div>
<!-- Raw JSON Mode -->
<div id="raw-json-mode" class="query-mode hidden">
<div class="form-group">
<label class="form-label">JSON запрос</label>
<textarea
id="json-textarea"
class="form-textarea"
rows="10"
placeholder='[&#10; { "body": "Какая текущая ВВП в России?", "with_docs": true }&#10;]'></textarea>
<div class="form-helper-text" id="json-validation-message"></div>
</div>
<div class="flex gap-sm">
<button class="btn btn-outlined" id="load-request-btn">
<span class="material-icons">upload_file</span>
Загрузить из файла
</button>
<button class="btn btn-text" id="validate-json-btn">
<span class="material-icons">check_circle</span>
Проверить JSON
</button>
</div>
</div>
</div>
<div class="card-actions">
<button class="btn btn-outlined" id="load-response-btn">
<span class="material-icons">folder_open</span>
Загрузить ответ (Response)
</button>
<button class="btn btn-filled" id="send-query-btn">
<span class="material-icons">send</span>
Отправить запрос
</button>
</div>
</div>
</div>
<!-- Answer Viewer (shown when data is available) -->
<div id="answer-viewer" class="hidden">
<!-- Question Header -->
<div class="card mb-md">
<div class="card-content">
<div class="text-overline mb-sm">Вопрос #<span id="current-question-number">1</span></div>
<h4 id="current-question-text"></h4>
</div>
</div>
<!-- Metadata Card -->
<div class="card mb-md">
<div class="card-header">
<h6 class="card-title">Метаданные</h6>
</div>
<div class="card-content">
<div class="metadata-grid">
<div class="metadata-item">
<div class="metadata-label">Время обработки</div>
<div class="metadata-value" id="processing-time">-</div>
</div>
<div class="metadata-item">
<div class="metadata-label">Request ID</div>
<div class="metadata-value" id="request-id">-</div>
</div>
<div class="metadata-item">
<div class="metadata-label">Timestamp</div>
<div class="metadata-value" id="request-timestamp">-</div>
</div>
</div>
</div>
</div>
<!-- Answer Bodies -->
<div class="card mb-md">
<div class="card-header">
<h6 class="card-title">Тексты ответов</h6>
</div>
<div class="tabs">
<button class="tab active" data-tab="body-research">Research</button>
<button class="tab" data-tab="body-analytical">Analytical Hub</button>
</div>
<div id="body-research" class="tab-content active">
<div id="body-research-text"></div>
<!-- Annotation Section -->
<div class="annotation-section">
<h6 class="mb-sm">Пометки</h6>
<div class="annotation-issues mb-md">
<label class="issue-checkbox">
<input type="checkbox" class="checkbox" data-section="body_research" data-issue="factual_error">
<span>Факт. ошибка</span>
</label>
<label class="issue-checkbox">
<input type="checkbox" class="checkbox" data-section="body_research" data-issue="inaccurate_wording">
<span>Неточность формулировки</span>
</label>
<label class="issue-checkbox">
<input type="checkbox" class="checkbox" data-section="body_research" data-issue="insufficient_context">
<span>Недостаточно контекста</span>
</label>
<label class="issue-checkbox">
<input type="checkbox" class="checkbox" data-section="body_research" data-issue="offtopic">
<span>Не по вопросу</span>
</label>
<label class="issue-checkbox">
<input type="checkbox" class="checkbox" data-section="body_research" data-issue="technical_answer">
<span>Технический ответ</span>
</label>
</div>
<div class="form-group">
<label class="form-label">Комментарий</label>
<textarea class="form-textarea" rows="3" data-section="body_research" placeholder="Ваш комментарий..."></textarea>
</div>
</div>
</div>
<div id="body-analytical" class="tab-content">
<div id="body-analytical-text"></div>
<!-- Annotation Section -->
<div class="annotation-section">
<h6 class="mb-sm">Пометки</h6>
<div class="annotation-issues mb-md">
<label class="issue-checkbox">
<input type="checkbox" class="checkbox" data-section="body_analytical_hub" data-issue="factual_error">
<span>Факт. ошибка</span>
</label>
<label class="issue-checkbox">
<input type="checkbox" class="checkbox" data-section="body_analytical_hub" data-issue="inaccurate_wording">
<span>Неточность формулировки</span>
</label>
<label class="issue-checkbox">
<input type="checkbox" class="checkbox" data-section="body_analytical_hub" data-issue="insufficient_context">
<span>Недостаточно контекста</span>
</label>
<label class="issue-checkbox">
<input type="checkbox" class="checkbox" data-section="body_analytical_hub" data-issue="offtopic">
<span>Не по вопросу</span>
</label>
<label class="issue-checkbox">
<input type="checkbox" class="checkbox" data-section="body_analytical_hub" data-issue="technical_answer">
<span>Технический ответ</span>
</label>
</div>
<div class="form-group">
<label class="form-label">Комментарий</label>
<textarea class="form-textarea" rows="3" data-section="body_analytical_hub" placeholder="Ваш комментарий..."></textarea>
</div>
</div>
</div>
</div>
<!-- Overall Rating -->
<div class="card mb-md">
<div class="card-header">
<h6 class="card-title">Общая оценка ответа</h6>
</div>
<div class="card-content">
<div class="form-group">
<label class="form-label" for="overall-rating">Оценка</label>
<select class="form-select" id="overall-rating" aria-label="Общая оценка ответа">
<option value="">Не оценено</option>
<option value="correct">Корректно</option>
<option value="partial">Частично корректно</option>
<option value="incorrect">Некорректно</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Общий комментарий</label>
<textarea class="form-textarea" rows="3" id="overall-comment" placeholder="Общий комментарий по ответу..."></textarea>
</div>
</div>
</div>
<!-- Documents Section -->
<div class="card">
<div class="card-header">
<h6 class="card-title">Документы</h6>
</div>
<div class="tabs">
<button class="tab active" data-tab="docs-vectorstore">Docs from Vectorstore</button>
<button class="tab" data-tab="docs-llm">Docs to LLM</button>
</div>
<!-- Docs from Vectorstore -->
<div id="docs-vectorstore" class="tab-content active">
<div class="tabs">
<button class="tab active" data-tab="vectorstore-research">Research</button>
<button class="tab" data-tab="vectorstore-analytical">Analytical Hub</button>
</div>
<div id="vectorstore-research" class="tab-content active">
<div id="vectorstore-research-docs"></div>
</div>
<div id="vectorstore-analytical" class="tab-content">
<div id="vectorstore-analytical-docs"></div>
</div>
</div>
<!-- Docs to LLM -->
<div id="docs-llm" class="tab-content">
<div class="tabs">
<button class="tab active" data-tab="llm-research">Research</button>
<button class="tab" data-tab="llm-analytical">Analytical Hub</button>
</div>
<div id="llm-research" class="tab-content active">
<div id="llm-research-docs"></div>
</div>
<div id="llm-analytical" class="tab-content">
<div id="llm-analytical-docs"></div>
</div>
</div>
</div>
</div>
</main>
</div>
</div>
<!-- Settings Dialog -->
<div class="dialog-overlay" id="settings-dialog">
<div class="dialog">
<div class="dialog-header">
<h5 class="dialog-title">Настройки</h5>
<button class="btn-icon" id="close-settings-btn">
<span class="material-icons">close</span>
</button>
</div>
<div class="dialog-content">
<!-- Environment Selector -->
<div class="form-group">
<label class="form-label">Редактируемое окружение</label>
<select class="form-select" id="settings-env-selector">
<option value="ift">ИФТ</option>
<option value="psi">ПСИ</option>
<option value="prod">ПРОМ</option>
</select>
</div>
<hr style="margin: var(--md-spacing-lg) 0; border: none; border-top: 1px solid var(--md-divider);">
<!-- Environment-specific settings -->
<div id="settings-env-fields">
<div class="form-group">
<label class="form-label">Режим API</label>
<select class="form-select" id="setting-api-mode">
<option value="bench">Bench (батч-тестирование)</option>
<option value="backend">Backend (имитация бота)</option>
</select>
<div class="form-helper-text">Bench - отправка массива вопросов; Backend - вопросы по одному</div>
</div>
<div class="form-group">
<label class="form-label">Bearer Token (необязательно)</label>
<input type="password" class="form-input" id="setting-bearer-token" placeholder="your-bearer-token">
<div class="form-helper-text">Токен для авторизации запросов к RAG API</div>
</div>
<div class="form-group">
<label class="form-label">System-Platform (необязательно)</label>
<input type="text" class="form-input" id="setting-system-platform" placeholder="platform-name">
<div class="form-helper-text">Заголовок System-Platform для запросов</div>
</div>
<div class="form-group">
<label class="form-label">System-Platform-User (необязательно)</label>
<input type="text" class="form-input" id="setting-system-platform-user" placeholder="user-name">
<div class="form-helper-text">Заголовок System-Platform-User для запросов</div>
</div>
<h6 class="mt-lg mb-md" id="backend-settings-header" style="display: none;">Настройки Backend Mode</h6>
<div id="backend-settings" style="display: none;">
<div class="form-group">
<label class="form-label">Platform User ID</label>
<input type="text" class="form-input" id="setting-platform-user-id" placeholder="user-123">
<div class="form-helper-text">Идентификатор пользователя платформы</div>
</div>
<div class="form-group">
<label class="form-label">Platform ID</label>
<input type="text" class="form-input" id="setting-platform-id" placeholder="platform-1">
<div class="form-helper-text">Идентификатор платформы</div>
</div>
<div class="form-group">
<label class="form-label" style="display: flex; align-items: center; gap: 8px;">
<input type="checkbox" id="setting-with-classify" class="checkbox">
Включить классификацию вопросов
</label>
<div class="form-helper-text">Запрашивать question_type в ответе</div>
</div>
<div class="form-group">
<label class="form-label" style="display: flex; align-items: center; gap: 8px;">
<input type="checkbox" id="setting-reset-session-mode" class="checkbox" checked>
Сбрасывать сессию после каждого вопроса
</label>
<div class="form-helper-text">Если выключено, вопросы будут задаваться в рамках одной сессии</div>
</div>
</div>
</div>
</div>
<div class="dialog-actions">
<button class="btn btn-text" id="import-settings-btn">
<span class="material-icons">upload</span>
Импорт настроек
</button>
<button class="btn btn-text" id="export-settings-btn">
<span class="material-icons">download</span>
Экспорт настроек
</button>
<button class="btn btn-outlined" id="reset-settings-btn">Сброс</button>
<button class="btn btn-filled" id="save-settings-btn">Сохранить</button>
</div>
</div>
</div>
<!-- Loading Overlay -->
<div class="loading-overlay" id="loading-overlay">
<div class="spinner"></div>
<div class="loading-message" id="loading-message">Отправка запроса...</div>
<div class="loading-submessage">Это может занять до 30 минут</div>
</div>
<!-- Scripts -->
<script src="settings.js"></script>
<script src="api-client.js"></script>
<script src="app.js"></script>
</body>
</html>

70
static/settings.js Normal file
View File

@ -0,0 +1,70 @@
// Default settings for Brief Bench (FastAPI Version)
// User-editable fields only - server configuration managed by FastAPI backend
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'
};
// Export for use in app.js
if (typeof module !== 'undefined' && module.exports) {
module.exports = defaultSettings;
}

1166
static/styles.css Normal file

File diff suppressed because it is too large Load Diff