247 lines
8.4 KiB
JavaScript
247 lines
8.4 KiB
JavaScript
/**
|
||
* Answer Viewer UI
|
||
*
|
||
* UI компонент для просмотра ответов.
|
||
*/
|
||
|
||
import appState from '../state/appState.js'
|
||
import { escapeHtml, formatTime, formatTimestamp, isTableText, parseTextTable } from '../utils/format.utils.js'
|
||
import { setElementText, setElementHTML, addClass, removeClass, hideElement, showElement } from '../utils/dom.utils.js'
|
||
|
||
/**
|
||
* Показать просмотрщик ответов, скрыть построитель запросов
|
||
*/
|
||
export function show() {
|
||
const queryBuilder = document.getElementById('query-builder')
|
||
const answerViewer = document.getElementById('answer-viewer')
|
||
|
||
if (queryBuilder) {
|
||
addClass(queryBuilder, 'hidden')
|
||
}
|
||
|
||
if (answerViewer) {
|
||
removeClass(answerViewer, 'hidden')
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Отрендерить ответ по индексу
|
||
* @param {number} index - Индекс ответа
|
||
* @param {Function} onLoadAnnotations - Callback для загрузки аннотаций
|
||
*/
|
||
export function render(index, onLoadAnnotations) {
|
||
const env = appState.getCurrentEnv()
|
||
const answer = env.currentResponse?.answers[index]
|
||
|
||
if (!answer) {
|
||
console.error('Answer not found at index:', index)
|
||
return
|
||
}
|
||
|
||
const isBackendMode = answer.backend_mode === true
|
||
|
||
// Show answer viewer
|
||
show()
|
||
|
||
// Render question header
|
||
setElementText('current-question-number', index + 1)
|
||
setElementText('current-question-text', answer.question)
|
||
|
||
// Render metadata
|
||
setElementText('processing-time', isBackendMode ? 'N/A' : formatTime(answer.processing_time_sec))
|
||
setElementText('request-id', env.requestId || '-')
|
||
setElementText('request-timestamp', env.requestTimestamp ? formatTimestamp(env.requestTimestamp) : '-')
|
||
|
||
// Render answer bodies
|
||
renderBody('body-research-text', answer.body_research)
|
||
renderBody('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) {
|
||
if (isBackendMode) {
|
||
hideElement(docsSection)
|
||
} else {
|
||
showElement(docsSection)
|
||
}
|
||
}
|
||
|
||
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
|
||
if (typeof onLoadAnnotations === 'function') {
|
||
onLoadAnnotations(index)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Отрендерить тело ответа
|
||
* @param {string} elementId - ID элемента
|
||
* @param {string} text - Текст ответа
|
||
*/
|
||
export function renderBody(elementId, text) {
|
||
const container = document.getElementById(elementId)
|
||
|
||
if (!container) {
|
||
console.warn(`Element ${elementId} not found`)
|
||
return
|
||
}
|
||
|
||
if (!text) {
|
||
setElementHTML(container, '<p class="color-warning">Нет данных</p>')
|
||
return
|
||
}
|
||
|
||
if (isTableText(text)) {
|
||
const table = parseTextTable(text)
|
||
if (table) {
|
||
setElementHTML(container, `<div class="table-container">${table}</div>`)
|
||
return
|
||
}
|
||
}
|
||
|
||
// Render as plain text with line breaks
|
||
const html = `<p>${escapeHtml(text).replace(/\n/g, '<br>')}</p>`
|
||
setElementHTML(container, html)
|
||
}
|
||
|
||
/**
|
||
* Отрендерить документы
|
||
* @param {string} containerId - ID контейнера
|
||
* @param {Array} docs - Массив документов
|
||
* @param {string} section - Секция (docs_from_vectorstore, docs_to_llm)
|
||
* @param {string} subsection - Подсекция (research, analytical_hub)
|
||
* @param {number} answerIndex - Индекс ответа
|
||
*/
|
||
export function renderDocuments(containerId, docs, section, subsection, answerIndex) {
|
||
const container = document.getElementById(containerId)
|
||
|
||
if (!container) {
|
||
console.warn(`Container ${containerId} not found`)
|
||
return
|
||
}
|
||
|
||
if (!docs || docs.length === 0) {
|
||
setElementHTML(container, `
|
||
<div class="empty-state">
|
||
<div class="empty-state-text">Нет документов</div>
|
||
</div>
|
||
`)
|
||
return
|
||
}
|
||
|
||
const html = docs.map((doc, docIndex) => {
|
||
const docId = `doc-${section}-${subsection}-${docIndex}`
|
||
|
||
let docContent = ''
|
||
if (typeof doc === 'string') {
|
||
if (isTableText(doc)) {
|
||
const table = parseTextTable(doc)
|
||
docContent = table || `<pre class="text-table">${escapeHtml(doc)}</pre>`
|
||
} else {
|
||
docContent = `<p>${escapeHtml(doc).replace(/\n/g, '<br>')}</p>`
|
||
}
|
||
} else {
|
||
docContent = `<pre class="text-table">${escapeHtml(JSON.stringify(doc, null, 2))}</pre>`
|
||
}
|
||
|
||
return `
|
||
<div class="expansion-panel" id="${docId}">
|
||
<div class="expansion-header" onclick="window.toggleExpansion('${docId}')">
|
||
<span class="expansion-header-title">Документ #${docIndex + 1}</span>
|
||
<span class="material-icons expansion-icon">expand_more</span>
|
||
</div>
|
||
<div class="expansion-content">
|
||
<div class="expansion-body">
|
||
${docContent}
|
||
|
||
<div class="annotation-section mt-md">
|
||
<h6 class="mb-sm">Пометки</h6>
|
||
<div class="annotation-issues mb-md">
|
||
<label class="issue-checkbox">
|
||
<input type="checkbox" class="checkbox"
|
||
data-section="${section}"
|
||
data-subsection="${subsection}"
|
||
data-doc-index="${docIndex}"
|
||
data-issue="factual_error">
|
||
<span>Факт. ошибка</span>
|
||
</label>
|
||
<label class="issue-checkbox">
|
||
<input type="checkbox" class="checkbox"
|
||
data-section="${section}"
|
||
data-subsection="${subsection}"
|
||
data-doc-index="${docIndex}"
|
||
data-issue="inaccurate_wording">
|
||
<span>Неточность формулировки</span>
|
||
</label>
|
||
<label class="issue-checkbox">
|
||
<input type="checkbox" class="checkbox"
|
||
data-section="${section}"
|
||
data-subsection="${subsection}"
|
||
data-doc-index="${docIndex}"
|
||
data-issue="insufficient_context">
|
||
<span>Недостаточно контекста</span>
|
||
</label>
|
||
<label class="issue-checkbox">
|
||
<input type="checkbox" class="checkbox"
|
||
data-section="${section}"
|
||
data-subsection="${subsection}"
|
||
data-doc-index="${docIndex}"
|
||
data-issue="offtopic">
|
||
<span>Не по теме</span>
|
||
</label>
|
||
<label class="issue-checkbox">
|
||
<input type="checkbox" class="checkbox"
|
||
data-section="${section}"
|
||
data-subsection="${subsection}"
|
||
data-doc-index="${docIndex}"
|
||
data-issue="technical_answer">
|
||
<span>Технический ответ</span>
|
||
</label>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>Комментарий</label>
|
||
<textarea class="textarea"
|
||
data-section="${section}"
|
||
data-subsection="${subsection}"
|
||
data-doc-index="${docIndex}"
|
||
data-field="comment"
|
||
placeholder="Комментарий к документу..."></textarea>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`
|
||
}).join('')
|
||
|
||
setElementHTML(container, html)
|
||
}
|
||
|
||
/**
|
||
* Переключить раскрытие expansion panel
|
||
* @param {string} id - ID панели
|
||
*/
|
||
export function toggleExpansion(id) {
|
||
const panel = document.getElementById(id)
|
||
if (panel) {
|
||
panel.classList.toggle('expanded')
|
||
}
|
||
}
|
||
|
||
// Export as default object
|
||
export default {
|
||
show,
|
||
render,
|
||
renderBody,
|
||
renderDocuments,
|
||
toggleExpansion
|
||
}
|