ZMIANY BEZPIECZEŃSTWA: 1. Defense in depth w NordaBizChatEngine: - send_message() - wymaga user_id i weryfikuje właściciela rozmowy - get_conversation_history() - opcjonalna walidacja user_id - Logowanie prób nieautoryzowanego dostępu 2. Anonimizacja w panelu admina: - Usunięto wyświetlanie treści zapytań użytkowników - Zastąpiono statystykami: długość, kategoria tematyczna - Zachowano AI responses (publiczne dane firm) 3. Ochrona prywatności: - Użytkownicy NIE mogą zobaczyć zapytań innych użytkowników - Admini widzą tylko zanonimizowane statystyki - Audit logging dla prób nieautoryzowanego dostępu Odblokowuje zadanie #10 (Baza wiedzy Norda GPT). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
394 lines
14 KiB
HTML
Executable File
394 lines
14 KiB
HTML
Executable File
{% extends "base.html" %}
|
|
|
|
{% block title %}Analityka Chatu AI - Norda Biznes Hub{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
.analytics-header {
|
|
margin-bottom: var(--spacing-xl);
|
|
}
|
|
|
|
.analytics-header h1 {
|
|
font-size: var(--font-size-3xl);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: var(--spacing-lg);
|
|
margin-bottom: var(--spacing-2xl);
|
|
}
|
|
|
|
.stat-card {
|
|
background: var(--surface);
|
|
padding: var(--spacing-lg);
|
|
border-radius: var(--radius-lg);
|
|
box-shadow: var(--shadow);
|
|
text-align: center;
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: var(--font-size-3xl);
|
|
font-weight: 700;
|
|
color: var(--primary);
|
|
}
|
|
|
|
.stat-value.positive { color: var(--success); }
|
|
.stat-value.negative { color: var(--error); }
|
|
.stat-value.neutral { color: var(--secondary); }
|
|
|
|
.stat-label {
|
|
color: var(--text-secondary);
|
|
font-size: var(--font-size-sm);
|
|
margin-top: var(--spacing-xs);
|
|
}
|
|
|
|
.section {
|
|
background: var(--surface);
|
|
padding: var(--spacing-xl);
|
|
border-radius: var(--radius-lg);
|
|
box-shadow: var(--shadow);
|
|
margin-bottom: var(--spacing-xl);
|
|
}
|
|
|
|
.section h2 {
|
|
font-size: var(--font-size-xl);
|
|
margin-bottom: var(--spacing-lg);
|
|
color: var(--text-primary);
|
|
border-bottom: 2px solid var(--border);
|
|
padding-bottom: var(--spacing-sm);
|
|
}
|
|
|
|
.query-list {
|
|
list-style: none;
|
|
}
|
|
|
|
.query-item {
|
|
padding: var(--spacing-md);
|
|
border-bottom: 1px solid var(--border);
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.query-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.query-text {
|
|
flex: 1;
|
|
font-size: var(--font-size-base);
|
|
}
|
|
|
|
.query-meta {
|
|
color: var(--text-secondary);
|
|
font-size: var(--font-size-sm);
|
|
text-align: right;
|
|
}
|
|
|
|
.feedback-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: var(--spacing-xs);
|
|
padding: var(--spacing-xs) var(--spacing-sm);
|
|
border-radius: var(--radius);
|
|
font-size: var(--font-size-sm);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.feedback-badge.positive {
|
|
background: rgba(16, 185, 129, 0.1);
|
|
color: var(--success);
|
|
}
|
|
|
|
.feedback-badge.negative {
|
|
background: rgba(239, 68, 68, 0.1);
|
|
color: var(--error);
|
|
}
|
|
|
|
.satisfaction-bar {
|
|
height: 8px;
|
|
background: var(--border);
|
|
border-radius: 4px;
|
|
overflow: hidden;
|
|
margin-top: var(--spacing-sm);
|
|
}
|
|
|
|
.satisfaction-fill {
|
|
height: 100%;
|
|
background: linear-gradient(90deg, var(--error), var(--warning), var(--success));
|
|
transition: width 0.3s ease;
|
|
}
|
|
|
|
.response-preview {
|
|
background: var(--background);
|
|
padding: var(--spacing-md);
|
|
border-radius: var(--radius);
|
|
margin-top: var(--spacing-sm);
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
max-height: 100px;
|
|
overflow: hidden;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="analytics-header">
|
|
<h1>Analityka Chatu AI</h1>
|
|
<p class="text-muted">Monitoruj jakość odpowiedzi i zachowania użytkowników</p>
|
|
</div>
|
|
|
|
<!-- Stats Grid -->
|
|
<div class="stats-grid">
|
|
<div class="stat-card">
|
|
<div class="stat-value">{{ total_conversations }}</div>
|
|
<div class="stat-label">Rozmów</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value">{{ total_user_messages }}</div>
|
|
<div class="stat-label">Zapytań użytkowników</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value positive">{{ positive_feedback }}</div>
|
|
<div class="stat-label">Pozytywnych ocen</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value negative">{{ negative_feedback }}</div>
|
|
<div class="stat-label">Negatywnych ocen</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value {% if satisfaction_rate >= 70 %}positive{% elif satisfaction_rate >= 40 %}neutral{% else %}negative{% endif %}">
|
|
{{ satisfaction_rate }}%
|
|
</div>
|
|
<div class="stat-label">Satysfakcja</div>
|
|
<div class="satisfaction-bar">
|
|
<div class="satisfaction-fill" style="width: {{ satisfaction_rate }}%"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Query Statistics (Privacy Protected) -->
|
|
<div class="section">
|
|
<h2>Statystyki zapytań <span style="font-size: var(--font-size-sm); color: var(--text-secondary);">🔒 Dane zanonimizowane</span></h2>
|
|
|
|
<div class="stats-grid" style="margin-bottom: var(--spacing-lg);">
|
|
<div class="stat-card">
|
|
<div class="stat-value">{{ query_stats.total_today }}</div>
|
|
<div class="stat-label">Zapytań dzisiaj</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value">{{ query_stats.avg_length|int }}</div>
|
|
<div class="stat-label">Śr. długość (znaki)</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value">{{ query_stats.queries_with_company }}</div>
|
|
<div class="stat-label">O firmach</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value">{{ query_stats.queries_with_contact }}</div>
|
|
<div class="stat-label">O kontaktach</div>
|
|
</div>
|
|
</div>
|
|
|
|
<h3 style="margin-top: var(--spacing-lg);">Ostatnia aktywność (zanonimizowana)</h3>
|
|
{% if recent_queries %}
|
|
<ul class="query-list">
|
|
{% for query in recent_queries %}
|
|
<li class="query-item">
|
|
<div class="query-text">
|
|
<span style="color: var(--text-secondary);">●</span>
|
|
Zapytanie ({{ query.length }} znaków)
|
|
{% if query.has_company_mention %}
|
|
<span class="badge" style="background: var(--primary-light); color: var(--primary); padding: 2px 6px; border-radius: 4px; font-size: 10px; margin-left: 8px;">FIRMA</span>
|
|
{% endif %}
|
|
{% if query.has_contact_request %}
|
|
<span class="badge" style="background: var(--success-light); color: var(--success); padding: 2px 6px; border-radius: 4px; font-size: 10px; margin-left: 4px;">KONTAKT</span>
|
|
{% endif %}
|
|
</div>
|
|
<div class="query-meta">
|
|
{{ query.created_at.strftime('%d.%m %H:%M') }}
|
|
</div>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
{% else %}
|
|
<p class="text-muted">Brak zapytań</p>
|
|
{% endif %}
|
|
|
|
<div style="margin-top: var(--spacing-lg); padding: var(--spacing-md); background: var(--background); border-radius: var(--radius); font-size: var(--font-size-sm); color: var(--text-secondary);">
|
|
<strong>🔒 Ochrona prywatności:</strong> Treść zapytań użytkowników nie jest wyświetlana w panelu admina.
|
|
Widoczne są tylko zanonimizowane statystyki (długość, kategorie tematyczne).
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Feedback Section -->
|
|
<div class="section">
|
|
<h2>Odpowiedzi z oceną</h2>
|
|
{% if recent_feedback %}
|
|
<ul class="query-list">
|
|
{% for msg in recent_feedback %}
|
|
<li class="query-item" style="flex-direction: column; align-items: flex-start;">
|
|
<div style="display: flex; justify-content: space-between; width: 100%; align-items: center;">
|
|
<span class="feedback-badge {% if msg.feedback_rating == 2 %}positive{% else %}negative{% endif %}">
|
|
{% if msg.feedback_rating == 2 %}
|
|
<svg width="16" height="16" fill="currentColor" viewBox="0 0 20 20"><path d="M2 10.5a1.5 1.5 0 113 0v6a1.5 1.5 0 01-3 0v-6zM6 10.333v5.43a2 2 0 001.106 1.79l.05.025A4 4 0 008.943 18h5.416a2 2 0 001.962-1.608l1.2-6A2 2 0 0015.56 8H12V4a2 2 0 00-2-2 1 1 0 00-1 1v.667a4 4 0 01-.8 2.4L6.8 7.933a4 4 0 00-.8 2.4z"/></svg>
|
|
Pomocne
|
|
{% else %}
|
|
<svg width="16" height="16" fill="currentColor" viewBox="0 0 20 20"><path d="M18 9.5a1.5 1.5 0 11-3 0v-6a1.5 1.5 0 013 0v6zM14 9.667v-5.43a2 2 0 00-1.105-1.79l-.05-.025A4 4 0 0011.055 2H5.64a2 2 0 00-1.962 1.608l-1.2 6A2 2 0 004.44 12H8v4a2 2 0 002 2 1 1 0 001-1v-.667a4 4 0 01.8-2.4l1.4-1.866a4 4 0 00.8-2.4z"/></svg>
|
|
Do poprawy
|
|
{% endif %}
|
|
</span>
|
|
<span class="query-meta">{{ msg.feedback_at.strftime('%d.%m %H:%M') if msg.feedback_at else '' }}</span>
|
|
</div>
|
|
<div class="response-preview">{{ msg.content[:300] }}{% if msg.content|length > 300 %}...{% endif %}</div>
|
|
{% if msg.feedback_comment %}
|
|
<div style="margin-top: var(--spacing-sm); font-size: var(--font-size-sm);">
|
|
<strong>Komentarz:</strong> {{ msg.feedback_comment }}
|
|
</div>
|
|
{% endif %}
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
{% else %}
|
|
<p class="text-muted">Brak ocen - popros uzytkownikow o feedback!</p>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- AI Learning Status Section -->
|
|
<div class="section">
|
|
<h2>Uczenie AI z feedbacku</h2>
|
|
<div id="learningStatus">
|
|
<p class="text-muted">Ladowanie statusu uczenia...</p>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.learning-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: var(--spacing-lg);
|
|
margin-bottom: var(--spacing-lg);
|
|
}
|
|
.learning-card {
|
|
background: var(--background);
|
|
padding: var(--spacing-lg);
|
|
border-radius: var(--radius);
|
|
text-align: center;
|
|
}
|
|
.learning-card.active {
|
|
border-left: 4px solid var(--success);
|
|
}
|
|
.learning-card.seed {
|
|
border-left: 4px solid var(--warning);
|
|
}
|
|
.learning-value {
|
|
font-size: var(--font-size-2xl);
|
|
font-weight: 700;
|
|
color: var(--primary);
|
|
}
|
|
.learning-label {
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
}
|
|
.example-card {
|
|
background: var(--background);
|
|
padding: var(--spacing-md);
|
|
border-radius: var(--radius);
|
|
margin-bottom: var(--spacing-md);
|
|
border-left: 3px solid var(--success);
|
|
}
|
|
.example-card.negative {
|
|
border-left-color: var(--error);
|
|
}
|
|
.example-query {
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
margin-bottom: var(--spacing-xs);
|
|
}
|
|
.example-response {
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
// Load AI Learning Status
|
|
async function loadLearningStatus() {
|
|
try {
|
|
const response = await fetch('/api/admin/ai-learning-status');
|
|
const data = await response.json();
|
|
|
|
if (!data.success) {
|
|
document.getElementById('learningStatus').innerHTML =
|
|
'<p class="text-muted">Blad ladowania statusu</p>';
|
|
return;
|
|
}
|
|
|
|
if (!data.learning_active) {
|
|
document.getElementById('learningStatus').innerHTML =
|
|
'<p class="text-muted">Uczenie z feedbacku nieaktywne</p>';
|
|
return;
|
|
}
|
|
|
|
const stats = data.stats || {};
|
|
const usingSeed = data.using_seed_examples;
|
|
|
|
let html = `
|
|
<div class="learning-grid">
|
|
<div class="learning-card ${usingSeed ? 'seed' : 'active'}">
|
|
<div class="learning-value">${usingSeed ? 'Seed' : 'Aktywne'}</div>
|
|
<div class="learning-label">${usingSeed ? 'Uzywa przykladow startowych' : 'Uczy sie z feedbacku'}</div>
|
|
</div>
|
|
<div class="learning-card">
|
|
<div class="learning-value">${data.positive_examples_count}</div>
|
|
<div class="learning-label">Pozytywnych przykladow</div>
|
|
</div>
|
|
<div class="learning-card">
|
|
<div class="learning-value">${stats.feedback_rate || 0}%</div>
|
|
<div class="learning-label">Wskaznik feedbacku</div>
|
|
</div>
|
|
<div class="learning-card">
|
|
<div class="learning-value">${stats.positive_rate || 0}%</div>
|
|
<div class="learning-label">Pozytywnych ocen</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Show positive examples
|
|
if (data.positive_examples && data.positive_examples.length > 0) {
|
|
html += '<h3 style="margin: var(--spacing-lg) 0 var(--spacing-md);">Przyklady uzywane do nauki</h3>';
|
|
for (const ex of data.positive_examples.slice(0, 3)) {
|
|
html += `
|
|
<div class="example-card">
|
|
<div class="example-query">Q: ${ex.query}</div>
|
|
<div class="example-response">${ex.response}</div>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
// Show patterns to avoid
|
|
if (data.negative_patterns && data.negative_patterns.length > 0) {
|
|
html += '<h3 style="margin: var(--spacing-lg) 0 var(--spacing-md);">Wzorce do unikania</h3>';
|
|
html += '<ul style="color: var(--error); font-size: var(--font-size-sm);">';
|
|
for (const pattern of data.negative_patterns) {
|
|
html += `<li>${pattern}</li>`;
|
|
}
|
|
html += '</ul>';
|
|
}
|
|
|
|
document.getElementById('learningStatus').innerHTML = html;
|
|
} catch (error) {
|
|
console.error('Error loading learning status:', error);
|
|
document.getElementById('learningStatus').innerHTML =
|
|
'<p class="text-muted">Blad ladowania statusu</p>';
|
|
}
|
|
}
|
|
|
|
// Load on page load
|
|
loadLearningStatus();
|
|
{% endblock %}
|