v1.8.0: YandexGPT-only NLP, unknown questions system, callback dedup, consent flow fixes, DELETE cascade, UTC+3, auto-detect sidebar menu, tel:/mailto: links

This commit is contained in:
2026-06-01 18:07:13 +03:00
parent aa1269e013
commit cc87bcf72c
20 changed files with 1453 additions and 301 deletions
@@ -0,0 +1,190 @@
{% extends "page.html" %}
{% block page_content %}
<style>
.uq-table { width:100%; border-collapse:collapse; }
.uq-table th, .uq-table td { padding:10px 14px; text-align:left; border-bottom:1px solid var(--border); font-size:13px; }
.uq-table th { font-weight:600; color:var(--text-muted); background:var(--bg-card); }
.uq-table td { vertical-align:top; }
.badge { display:inline-block; padding:2px 8px; border-radius:4px; font-size:11px; font-weight:600; }
.badge.new { background:#fff3cd; color:#856404; }
.badge.auto_added { background:#cce5ff; color:#004085; }
.badge.reviewed { background:#d4edda; color:#155724; }
.badge.ignored { background:#f8f9fa; color:#6c757d; }
.modal-overlay { display:none; position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.4); z-index:1000; align-items:center; justify-content:center; }
.modal-overlay.open { display:flex; }
.modal-box { background:var(--bg-card); border-radius:12px; padding:30px; width:650px; max-width:90vw; max-height:85vh; overflow-y:auto; }
.modal-box h3 { margin-bottom:20px; }
.form-group { margin-bottom:14px; }
.form-group label { display:block; font-weight:600; margin-bottom:4px; font-size:13px; color:var(--text-muted); }
.form-group input, .form-group textarea, .form-group select {
width:100%; padding:8px 12px; border:1px solid var(--border); border-radius:6px; font-size:14px; }
.form-group textarea { min-height:80px; resize:vertical; }
.modal-actions { display:flex; gap:12px; margin-top:16px; }
.filter-bar { display:flex; gap:12px; margin-bottom:16px; align-items:center; }
.filter-bar select { padding:8px 12px; border:1px solid var(--border); border-radius:6px; font-size:13px; }
</style>
<div class="card">
<div class="card-header"><h2>Неизвестные вопросы</h2></div>
<div class="filter-bar">
<select id="uq-status-filter" onchange="loadUQ()">
<option value="">Все статусы</option>
<option value="new">Новые</option>
<option value="auto_added">Авто-сгенерированные</option>
<option value="reviewed">Проверенные</option>
<option value="ignored">Игнорированные</option>
</select>
<span id="uq-count" style="font-size:13px;color:var(--text-muted);"></span>
</div>
<table class="uq-table">
<thead>
<tr>
<th>Вопрос</th>
<th>Смысл (NLP)</th>
<th>Раз</th>
<th>Статус</th>
<th>Сгенерированный ответ</th>
<th>Дата</th>
<th>Действия</th>
</tr>
</thead>
<tbody id="uq-table-body">
<tr><td colspan="7" style="text-align:center;padding:30px;color:var(--text-muted);">Загрузка...</td></tr>
</tbody>
</table>
</div>
<div class="modal-overlay" id="uq-modal">
<div class="modal-box">
<h3 id="uq-modal-title">Проверить вопрос</h3>
<form id="uq-form">
<input type="hidden" id="uq-edit-id">
<div class="form-group">
<label>Исходный вопрос</label>
<input type="text" id="uq-original" readonly style="background:var(--bg-secondary,#f5f5f5);">
</div>
<div class="form-group">
<label>Канонический вопрос</label>
<input type="text" id="uq-question" required>
</div>
<div class="form-group">
<label>Ответ</label>
<textarea id="uq-answer" required></textarea>
</div>
<div class="form-group">
<label>Ключевые слова (через запятую)</label>
<input type="text" id="uq-keywords">
</div>
<div class="form-group">
<label>Категория</label>
<select id="uq-category">
<option value="1">О компании</option>
<option value="2">Аудит безопасности</option>
<option value="3">Сервисное обслуживание (SLA)</option>
<option value="4">Реагирование на инциденты</option>
<option value="5">Технический надзор</option>
<option value="6">Документация и нормативы</option>
<option value="7">Риск-инжиниринг</option>
<option value="8">Отрасли и кейсы</option>
</select>
</div>
<div class="modal-actions">
<button type="submit" class="btn btn-primary">Опубликовать в базу знаний</button>
<button type="button" class="btn" onclick="closeModal()">Отмена</button>
</div>
</form>
</div>
</div>
<script>
var allUQ = [];
function loadUQ(){
fetch('/api/bot/unknown-questions').then(function(r){ return r.json() }).then(function(data){
allUQ = data;
filterUQ();
});
}
function filterUQ(){
var status = document.getElementById('uq-status-filter').value;
var filtered = allUQ;
if(status) filtered = allUQ.filter(function(q){ return q.status === status; });
document.getElementById('uq-count').textContent = filtered.length + ' из ' + allUQ.length;
renderUQ(filtered);
}
function renderUQ(items){
var tbody = document.getElementById('uq-table-body');
if(!items.length){
tbody.innerHTML = '<tr><td colspan="7" style="text-align:center;padding:30px;color:var(--text-muted);">Нет вопросов</td></tr>';
return;
}
var html = '';
items.forEach(function(q){
var statusLabels = {new:'Новый',auto_added:'Авто',reviewed:'Проверен',ignored:'Игнор'};
html += '<tr>' +
'<td><strong>' + esc(q.question_text) + '</strong></td>' +
'<td>' + esc(q.normalized_text || '—') + '</td>' +
'<td style="text-align:center;">' + q.ask_count + '</td>' +
'<td><span class="badge ' + q.status + '">' + (statusLabels[q.status]||q.status) + '</span></td>' +
'<td style="font-size:12px;">' + esc((q.generated_answer||'').substring(0,80)) + (q.generated_answer&&q.generated_answer.length>80?'...':'') + '</td>' +
'<td style="font-size:12px;">' + (q.created_at ? q.created_at.substring(0,10) : '—') + '</td>' +
'<td>';
if(q.status === 'new' || q.status === 'auto_added'){
html += '<button class="btn btn-sm" onclick="openReview(' + q.id + ')" title="Проверить">✏️</button> ';
html += '<button class="btn btn-sm" onclick="ignoreUQ(' + q.id + ')" style="color:#dc3545;" title="Игнорировать">🗑</button>';
}
html += '</td></tr>';
});
tbody.innerHTML = html;
}
function openReview(id){
var q = allUQ.find(function(x){ return x.id === id; });
if(!q) return;
document.getElementById('uq-edit-id').value = id;
document.getElementById('uq-original').value = q.question_text;
document.getElementById('uq-question').value = q.normalized_text || q.question_text;
document.getElementById('uq-answer').value = q.generated_answer || '';
document.getElementById('uq-keywords').value = (q.generated_keywords || []).join(', ');
document.getElementById('uq-category').value = q.generated_category_id || '1';
document.getElementById('uq-modal-title').textContent = 'Проверить вопрос #' + id;
document.getElementById('uq-modal').classList.add('open');
}
function closeModal(){
document.getElementById('uq-modal').classList.remove('open');
}
document.getElementById('uq-form').addEventListener('submit', function(e){
e.preventDefault();
var id = document.getElementById('uq-edit-id').value;
var data = {
question: document.getElementById('uq-question').value,
answer: document.getElementById('uq-answer').value,
keywords: document.getElementById('uq-keywords').value.split(',').map(function(s){ return s.trim(); }).filter(Boolean),
category_id: parseInt(document.getElementById('uq-category').value),
};
fetch('/api/bot/unknown-questions/' + id + '/edit', {
method: 'PUT', headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
}).then(function(r){ return r.json() }).then(function(d){
if(d.ok){ closeModal(); loadUQ(); showNotification('Опубликовано в базу знаний', 'success'); }
else showNotification('Ошибка', 'error');
});
});
function ignoreUQ(id){
if(!confirm('Игнорировать вопрос?')) return;
fetch('/api/bot/unknown-questions/' + id + '/ignore', { method: 'PUT' })
.then(function(r){ return r.json() }).then(function(d){
if(d.ok){ loadUQ(); showNotification('Игнорировано', 'success'); }
});
}
function esc(s){ return s ? s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;') : ''; }
loadUQ();
</script>
{% endblock %}