nordabiz/templates/admin/zopk_knowledge_dashboard.html
Maciej Pienczyn 094379d95e
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
fix(templates): Add blueprint prefix to url_for calls across admin templates
After refactoring to blueprints, templates still used bare endpoint names
(e.g., url_for('admin_zopk')) instead of prefixed names (e.g.,
url_for('admin.admin_zopk')). While most worked via backward-compat aliases,
api_zopk_search_news was missing from the alias list causing 500 on /admin/zopk.

Fixed 19 template files and added missing alias for api_zopk_search_news.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 13:44:50 +01:00

1235 lines
39 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "base.html" %}
{% block title %}Baza Wiedzy ZOPK - Panel Admina{% endblock %}
{% block extra_css %}
<style>
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-xl);
flex-wrap: wrap;
gap: var(--spacing-md);
}
.page-header h1 {
font-size: var(--font-size-2xl);
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.header-actions {
display: flex;
gap: var(--spacing-sm);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: var(--spacing-lg);
margin-bottom: var(--spacing-xl);
}
.stat-card {
background: var(--surface);
border-radius: var(--radius-lg);
padding: var(--spacing-lg);
box-shadow: var(--shadow);
text-align: center;
transition: var(--transition);
text-decoration: none;
color: inherit;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
}
.stat-card.clickable {
cursor: pointer;
}
.stat-icon {
font-size: 2.5rem;
margin-bottom: var(--spacing-sm);
}
.stat-value {
font-size: var(--font-size-2xl);
font-weight: 700;
color: var(--primary);
margin-bottom: var(--spacing-xs);
}
.stat-label {
color: var(--text-secondary);
font-size: var(--font-size-sm);
}
.stat-sublabel {
color: var(--text-tertiary);
font-size: var(--font-size-xs);
margin-top: var(--spacing-xs);
}
.section {
background: var(--surface);
border-radius: var(--radius-lg);
padding: var(--spacing-lg);
box-shadow: var(--shadow);
margin-bottom: var(--spacing-xl);
}
.section-title {
font-size: var(--font-size-lg);
font-weight: 600;
margin-bottom: var(--spacing-lg);
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.quick-links {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: var(--spacing-md);
}
.quick-link {
display: flex;
align-items: center;
gap: var(--spacing-md);
padding: var(--spacing-md);
background: var(--background);
border-radius: var(--radius);
text-decoration: none;
color: var(--text-primary);
transition: var(--transition);
}
.quick-link:hover {
background: var(--primary);
color: white;
}
.quick-link-icon {
font-size: 1.5rem;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: var(--surface);
border-radius: var(--radius);
}
.quick-link:hover .quick-link-icon {
background: rgba(255,255,255,0.2);
}
.quick-link-text {
flex: 1;
}
.quick-link-title {
font-weight: 600;
font-size: var(--font-size-base);
}
.quick-link-desc {
font-size: var(--font-size-xs);
opacity: 0.8;
}
.top-entities-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: var(--spacing-sm);
}
.entity-item {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm);
background: var(--background);
border-radius: var(--radius);
}
.entity-type-badge {
padding: 2px 8px;
border-radius: var(--radius-sm);
font-size: var(--font-size-xs);
font-weight: 500;
}
.entity-type-company { background: #dbeafe; color: #1e40af; }
.entity-type-person { background: #fce7f3; color: #be185d; }
.entity-type-place { background: #d1fae5; color: #065f46; }
.entity-type-organization { background: #fef3c7; color: #92400e; }
.entity-type-project { background: #e0e7ff; color: #3730a3; }
.entity-type-technology { background: #f3e8ff; color: #7c3aed; }
.entity-mentions {
margin-left: auto;
font-size: var(--font-size-sm);
color: var(--text-secondary);
}
.loading {
display: flex;
align-items: center;
justify-content: center;
padding: var(--spacing-xl);
color: var(--text-secondary);
}
.loading::after {
content: '';
width: 20px;
height: 20px;
border: 2px solid var(--border);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 1s linear infinite;
margin-left: var(--spacing-sm);
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.action-btn {
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--radius);
border: none;
cursor: pointer;
font-size: var(--font-size-sm);
font-weight: 500;
transition: var(--transition);
display: inline-flex;
align-items: center;
gap: var(--spacing-xs);
}
.action-btn-primary {
background: var(--primary);
color: white;
}
.action-btn-primary:hover {
background: var(--primary-dark);
}
.action-btn-secondary {
background: var(--background);
color: var(--text-primary);
border: 1px solid var(--border);
}
.action-btn-secondary:hover {
background: var(--surface);
}
.pipeline-status {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm) var(--spacing-md);
background: var(--background);
border-radius: var(--radius);
font-size: var(--font-size-sm);
}
.pipeline-status.running {
background: #fef3c7;
color: #92400e;
}
.pipeline-status.success {
background: #d1fae5;
color: #065f46;
}
.breadcrumb {
display: flex;
align-items: center;
gap: var(--spacing-xs);
font-size: var(--font-size-sm);
color: var(--text-secondary);
margin-bottom: var(--spacing-md);
}
.breadcrumb a {
color: var(--text-secondary);
text-decoration: none;
}
.breadcrumb a:hover {
color: var(--primary);
}
/* Toast notifications */
.toast-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 9999;
display: flex;
flex-direction: column;
gap: 10px;
max-width: 400px;
}
.toast {
background: var(--surface);
border-radius: var(--radius-lg);
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
padding: var(--spacing-md) var(--spacing-lg);
display: flex;
align-items: flex-start;
gap: var(--spacing-md);
animation: slideIn 0.3s ease-out;
border-left: 4px solid var(--primary);
}
.toast.success { border-left-color: #10b981; }
.toast.error { border-left-color: #ef4444; }
.toast.warning { border-left-color: #f59e0b; }
.toast.info { border-left-color: #3b82f6; }
.toast-icon {
font-size: 1.5rem;
flex-shrink: 0;
}
.toast-content {
flex: 1;
}
.toast-title {
font-weight: 600;
margin-bottom: 4px;
}
.toast-message {
font-size: var(--font-size-sm);
color: var(--text-secondary);
line-height: 1.4;
}
.toast-close {
background: none;
border: none;
cursor: pointer;
padding: 4px;
color: var(--text-tertiary);
font-size: 1.2rem;
}
.toast-close:hover {
color: var(--text-primary);
}
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOut {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
/* Modal */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
animation: fadeIn 0.2s ease-out;
}
.modal {
background: var(--surface);
border-radius: var(--radius-lg);
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
max-width: 440px;
width: 90%;
animation: scaleIn 0.2s ease-out;
}
.modal-header {
padding: var(--spacing-lg);
display: flex;
align-items: center;
gap: var(--spacing-md);
border-bottom: 1px solid var(--border);
}
.modal-icon {
font-size: 2rem;
}
.modal-title {
font-size: var(--font-size-lg);
font-weight: 600;
}
.modal-body {
padding: var(--spacing-lg);
color: var(--text-secondary);
line-height: 1.6;
}
.modal-footer {
padding: var(--spacing-md) var(--spacing-lg);
display: flex;
justify-content: flex-end;
gap: var(--spacing-sm);
border-top: 1px solid var(--border);
}
.modal-btn {
padding: 10px 20px;
border-radius: var(--radius);
font-weight: 500;
cursor: pointer;
border: none;
transition: var(--transition);
}
.modal-btn-secondary {
background: var(--background);
color: var(--text-primary);
border: 1px solid var(--border);
}
.modal-btn-secondary:hover {
background: var(--surface);
}
.modal-btn-primary {
background: var(--primary);
color: white;
}
.modal-btn-primary:hover {
background: var(--primary-dark);
}
.modal-btn-danger {
background: #ef4444;
color: white;
}
.modal-btn-danger:hover {
background: #dc2626;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes scaleIn {
from { transform: scale(0.95); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
/* Suggestions Modal - larger */
.modal-large {
max-width: 800px;
}
.suggestions-list {
max-height: 60vh;
overflow-y: auto;
}
.suggestion-card {
background: var(--background);
border-radius: var(--radius);
padding: var(--spacing-md);
margin-bottom: var(--spacing-md);
border-left: 4px solid var(--primary);
transition: var(--transition);
}
.suggestion-card:hover {
box-shadow: var(--shadow);
}
.suggestion-card.accepted {
border-left-color: #10b981;
opacity: 0.6;
}
.suggestion-card.rejected {
border-left-color: #ef4444;
opacity: 0.4;
text-decoration: line-through;
}
.suggestion-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-sm);
}
.suggestion-similarity {
background: linear-gradient(135deg, #8b5cf6 0%, #6366f1 100%);
color: white;
padding: 4px 10px;
border-radius: 20px;
font-size: var(--font-size-xs);
font-weight: 600;
}
.suggestion-type {
font-size: var(--font-size-xs);
color: var(--text-tertiary);
text-transform: uppercase;
}
.suggestion-text {
font-size: var(--font-size-sm);
line-height: 1.5;
margin-bottom: var(--spacing-sm);
}
.suggestion-match {
font-size: var(--font-size-xs);
color: var(--text-tertiary);
background: var(--surface);
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: var(--radius-sm);
margin-bottom: var(--spacing-sm);
}
.suggestion-match strong {
color: var(--text-secondary);
}
.suggestion-actions {
display: flex;
gap: var(--spacing-sm);
justify-content: flex-end;
}
.btn-accept {
background: #10b981;
color: white;
border: none;
padding: 6px 16px;
border-radius: var(--radius);
cursor: pointer;
font-size: var(--font-size-sm);
font-weight: 500;
transition: var(--transition);
}
.btn-accept:hover {
background: #059669;
}
.btn-reject {
background: var(--surface);
color: var(--text-secondary);
border: 1px solid var(--border);
padding: 6px 16px;
border-radius: var(--radius);
cursor: pointer;
font-size: var(--font-size-sm);
transition: var(--transition);
}
.btn-reject:hover {
background: #fee2e2;
border-color: #ef4444;
color: #ef4444;
}
.suggestions-summary {
display: flex;
gap: var(--spacing-lg);
padding: var(--spacing-md);
background: var(--background);
border-radius: var(--radius);
margin-bottom: var(--spacing-md);
}
.summary-stat {
text-align: center;
}
.summary-stat-value {
font-size: var(--font-size-xl);
font-weight: 700;
color: var(--primary);
}
.summary-stat-label {
font-size: var(--font-size-xs);
color: var(--text-tertiary);
}
.empty-suggestions {
text-align: center;
padding: var(--spacing-xl);
color: var(--text-secondary);
}
.empty-suggestions-icon {
font-size: 3rem;
margin-bottom: var(--spacing-md);
}
</style>
{% endblock %}
{% block content %}
<!-- Toast container -->
<div id="toastContainer" class="toast-container"></div>
<!-- Suggestions Review Modal -->
<div id="suggestionsModal" class="modal-overlay" style="display: none;">
<div class="modal modal-large">
<div class="modal-header">
<div class="modal-icon">🧠</div>
<div class="modal-title">Wyniki uczenia z weryfikacji</div>
</div>
<div class="modal-body">
<div class="suggestions-summary">
<div class="summary-stat">
<div class="summary-stat-value" id="suggestionsTotal">0</div>
<div class="summary-stat-label">Zweryfikowano</div>
</div>
<div class="summary-stat">
<div class="summary-stat-value" id="suggestionsPatterns">0</div>
<div class="summary-stat-label">Wzorców użyto</div>
</div>
<div class="summary-stat">
<div class="summary-stat-value" id="suggestionsUndone">0</div>
<div class="summary-stat-label">Cofnięto</div>
</div>
</div>
<div id="suggestionsList" class="suggestions-list">
<!-- Dynamically populated -->
</div>
</div>
<div class="modal-footer">
<button class="modal-btn modal-btn-secondary" onclick="closeSuggestionsModal()">Zamknij</button>
</div>
</div>
</div>
<div class="container">
<div class="breadcrumb">
<a href="{{ url_for('admin.admin_zopk') }}">Panel Admina</a>
<span></span>
<a href="{{ url_for('admin.admin_zopk') }}">ZOP Kaszubia</a>
<span></span>
<span>Baza Wiedzy</span>
</div>
<div class="page-header">
<h1>🧠 Baza Wiedzy ZOPK</h1>
<div class="header-actions">
<div id="pipelineStatus" class="pipeline-status">
<span></span>
<span>Ładowanie...</span>
</div>
</div>
</div>
<!-- Stats Grid -->
<div class="stats-grid" id="statsGrid">
<div class="loading">Ładowanie statystyk...</div>
</div>
<!-- Quick Links -->
<div class="section">
<h2 class="section-title">📋 Przegląd danych</h2>
<div class="quick-links">
<a href="{{ url_for('admin.admin_zopk_knowledge_chunks') }}" class="quick-link">
<div class="quick-link-icon">📄</div>
<div class="quick-link-text">
<div class="quick-link-title">Chunks (Fragmenty)</div>
<div class="quick-link-desc">Fragmenty tekstu z embeddingami</div>
</div>
</a>
<a href="{{ url_for('admin.admin_zopk_knowledge_facts') }}" class="quick-link">
<div class="quick-link-icon">📌</div>
<div class="quick-link-text">
<div class="quick-link-title">Fakty</div>
<div class="quick-link-desc">Wyekstraktowane fakty strukturalne</div>
</div>
</a>
<a href="{{ url_for('admin.admin_zopk_knowledge_entities') }}" class="quick-link">
<div class="quick-link-icon">🏢</div>
<div class="quick-link-text">
<div class="quick-link-title">Encje</div>
<div class="quick-link-desc">Firmy, osoby, miejsca, projekty</div>
</div>
</a>
<a href="{{ url_for('admin.admin_zopk_knowledge_duplicates') }}" class="quick-link" style="border-color: #f59e0b;">
<div class="quick-link-icon">🔀</div>
<div class="quick-link-text">
<div class="quick-link-title">Duplikaty</div>
<div class="quick-link-desc">Łączenie podobnych encji</div>
</div>
</a>
<a href="{{ url_for('admin.admin_zopk_knowledge_graph') }}" class="quick-link" style="border-color: #8b5cf6;">
<div class="quick-link-icon">🕸️</div>
<div class="quick-link-text">
<div class="quick-link-title">Graf relacji</div>
<div class="quick-link-desc">Wizualizacja powiązań</div>
</div>
</a>
<a href="{{ url_for('admin.admin_zopk_news') }}" class="quick-link">
<div class="quick-link-icon">📰</div>
<div class="quick-link-text">
<div class="quick-link-title">Źródłowe artykuły</div>
<div class="quick-link-desc">Newsy ZOPK (źródło wiedzy)</div>
</div>
</a>
<a href="{{ url_for('admin.admin_zopk_fact_duplicates') }}" class="quick-link" style="border-color: #f59e0b;">
<div class="quick-link-icon">🔀</div>
<div class="quick-link-text">
<div class="quick-link-title">Duplikaty faktów</div>
<div class="quick-link-desc">Łączenie podobnych faktów</div>
</div>
</a>
<a href="{{ url_for('admin.admin_zopk_timeline') }}" class="quick-link" style="border-color: #10b981;">
<div class="quick-link-icon">🗺️</div>
<div class="quick-link-text">
<div class="quick-link-title">Timeline ZOPK</div>
<div class="quick-link-desc">Roadmapa projektu</div>
</div>
</a>
</div>
</div>
<!-- Top Entities -->
<div class="section">
<h2 class="section-title">🏆 Top 10 encji według wzmianek</h2>
<div id="topEntities" class="top-entities-grid">
<div class="loading">Ładowanie encji...</div>
</div>
</div>
<!-- Actions -->
<div class="section">
<h2 class="section-title">⚡ Akcje</h2>
<div class="quick-links">
<div class="quick-link" onclick="runExtraction()" style="cursor: pointer;">
<div class="quick-link-icon">🔄</div>
<div class="quick-link-text">
<div class="quick-link-title">Uruchom ekstrakcję</div>
<div class="quick-link-desc">Przetwórz nowe artykuły</div>
</div>
</div>
<div class="quick-link" onclick="generateEmbeddings()" style="cursor: pointer;">
<div class="quick-link-icon">🧲</div>
<div class="quick-link-text">
<div class="quick-link-title">Generuj embeddingi</div>
<div class="quick-link-desc">Wektory dla chunków bez embeddingów</div>
</div>
</div>
<div class="quick-link" onclick="autoVerifyEntities()" style="cursor: pointer; border-left: 3px solid #10b981;">
<div class="quick-link-icon"></div>
<div class="quick-link-text">
<div class="quick-link-title">Auto-weryfikuj encje</div>
<div class="quick-link-desc">Zweryfikuj encje z ≥5 wzmiankami</div>
</div>
</div>
<div class="quick-link" onclick="autoVerifyFacts()" style="cursor: pointer; border-left: 3px solid #3b82f6;">
<div class="quick-link-icon">📌</div>
<div class="quick-link-text">
<div class="quick-link-title">Auto-weryfikuj fakty</div>
<div class="quick-link-desc">Zweryfikuj fakty z ważnością ≥70%</div>
</div>
</div>
<div class="quick-link" onclick="learnFromVerified()" style="cursor: pointer; border-left: 3px solid #8b5cf6;">
<div class="quick-link-icon">🧠</div>
<div class="quick-link-text">
<div class="quick-link-title">Ucz się z weryfikacji</div>
<div class="quick-link-desc">Znajdź podobne do zweryfikowanych faktów</div>
</div>
</div>
<a href="{{ url_for('admin.admin_zopk') }}" class="quick-link">
<div class="quick-link-icon">📊</div>
<div class="quick-link-text">
<div class="quick-link-title">Dashboard ZOPK</div>
<div class="quick-link-desc">Powrót do głównego panelu</div>
</div>
</a>
</div>
</div>
<!-- Verification Stats -->
<div class="section">
<h2 class="section-title">✅ Status weryfikacji</h2>
<div id="verificationStats" class="stats-grid" style="grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));">
<div class="loading">Ładowanie statystyk weryfikacji...</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
// ===== Toast & Modal System =====
function showToast(type, title, message, duration = 5000) {
const container = document.getElementById('toastContainer');
const icons = { success: '✅', error: '❌', warning: '⚠️', info: '' };
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.innerHTML = `
<div class="toast-icon">${icons[type] || ''}</div>
<div class="toast-content">
<div class="toast-title">${title}</div>
<div class="toast-message">${message}</div>
</div>
<button class="toast-close" onclick="this.parentElement.remove()">×</button>
`;
container.appendChild(toast);
if (duration > 0) {
setTimeout(() => {
toast.style.animation = 'slideOut 0.3s ease-out forwards';
setTimeout(() => toast.remove(), 300);
}, duration);
}
}
function showConfirm(title, message, icon = '❓') {
return new Promise((resolve) => {
const overlay = document.createElement('div');
overlay.className = 'modal-overlay';
overlay.innerHTML = `
<div class="modal">
<div class="modal-header">
<div class="modal-icon">${icon}</div>
<div class="modal-title">${title}</div>
</div>
<div class="modal-body">${message}</div>
<div class="modal-footer">
<button class="modal-btn modal-btn-secondary" data-action="cancel">Anuluj</button>
<button class="modal-btn modal-btn-primary" data-action="confirm">Potwierdź</button>
</div>
</div>
`;
document.body.appendChild(overlay);
overlay.addEventListener('click', (e) => {
if (e.target === overlay || e.target.dataset.action === 'cancel') {
overlay.remove();
resolve(false);
} else if (e.target.dataset.action === 'confirm') {
overlay.remove();
resolve(true);
}
});
});
}
// ===== Page Load =====
document.addEventListener('DOMContentLoaded', function() {
loadStats();
});
async function loadStats() {
try {
const response = await fetch('/admin/zopk/knowledge/stats');
const data = await response.json();
if (data.success) {
renderStats(data);
renderTopEntities(data.top_entities || []);
updatePipelineStatus(data);
} else {
document.getElementById('statsGrid').innerHTML = '<div class="loading">Błąd ładowania: ' + data.error + '</div>';
}
} catch (error) {
console.error('Error loading stats:', error);
document.getElementById('statsGrid').innerHTML = '<div class="loading">Błąd połączenia</div>';
}
}
function renderStats(data) {
const articles = data.articles || {};
const kb = data.knowledge_base || {};
const statsHtml = `
<a href="{{ url_for('admin.admin_zopk_knowledge_chunks') }}" class="stat-card clickable">
<div class="stat-icon">📄</div>
<div class="stat-value">${kb.total_chunks || 0}</div>
<div class="stat-label">Chunks</div>
<div class="stat-sublabel">${kb.chunks_with_embeddings || 0} z embeddingami</div>
</a>
<a href="{{ url_for('admin.admin_zopk_knowledge_facts') }}" class="stat-card clickable">
<div class="stat-icon">📌</div>
<div class="stat-value">${kb.total_facts || 0}</div>
<div class="stat-label">Fakty</div>
<div class="stat-sublabel">Wyekstraktowane informacje</div>
</a>
<a href="{{ url_for('admin.admin_zopk_knowledge_entities') }}" class="stat-card clickable">
<div class="stat-icon">🏢</div>
<div class="stat-value">${kb.total_entities || 0}</div>
<div class="stat-label">Encje</div>
<div class="stat-sublabel">Firmy, osoby, miejsca</div>
</a>
<div class="stat-card">
<div class="stat-icon">🔗</div>
<div class="stat-value">${kb.total_relations || 0}</div>
<div class="stat-label">Relacje</div>
<div class="stat-sublabel">Powiązania między encjami</div>
</div>
<div class="stat-card">
<div class="stat-icon">📰</div>
<div class="stat-value">${articles.extracted || 0}/${articles.scraped || 0}</div>
<div class="stat-label">Artykuły przetworzone</div>
<div class="stat-sublabel">${articles.pending_extract || 0} oczekuje</div>
</div>
<div class="stat-card">
<div class="stat-icon">🧲</div>
<div class="stat-value">${kb.chunks_with_embeddings || 0}/${kb.total_chunks || 0}</div>
<div class="stat-label">Embeddingi</div>
<div class="stat-sublabel">${kb.chunks_without_embeddings || 0} do wygenerowania</div>
</div>
`;
document.getElementById('statsGrid').innerHTML = statsHtml;
}
function renderTopEntities(entities) {
if (!entities.length) {
document.getElementById('topEntities').innerHTML = '<div class="loading">Brak encji w bazie</div>';
return;
}
const html = entities.map(e => `
<div class="entity-item">
<span class="entity-type-badge entity-type-${e.type}">${e.type}</span>
<span>${e.name}</span>
<span class="entity-mentions">${e.mentions}×</span>
</div>
`).join('');
document.getElementById('topEntities').innerHTML = html;
}
function updatePipelineStatus(data) {
const articles = data.articles || {};
const kb = data.knowledge_base || {};
let status = 'success';
let text = 'Pipeline OK';
if (articles.pending_extract > 0) {
status = 'running';
text = `${articles.pending_extract} artykułów czeka na ekstrakcję`;
} else if (kb.chunks_without_embeddings > 0) {
status = 'running';
text = `${kb.chunks_without_embeddings} chunków bez embeddingów`;
}
document.getElementById('pipelineStatus').className = 'pipeline-status ' + status;
document.getElementById('pipelineStatus').innerHTML = `
<span>${status === 'success' ? '✅' : '⏳'}</span>
<span>${text}</span>
`;
}
async function runExtraction() {
const confirmed = await showConfirm('Ekstrakcja wiedzy', 'Uruchomić ekstrakcję wiedzy z artykułów?', '🔄');
if (!confirmed) return;
try {
const response = await fetch('/admin/zopk/knowledge/extract', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() }}'
},
body: JSON.stringify({ limit: 50 })
});
const data = await response.json();
if (data.success !== false) {
showToast('success', 'Ekstrakcja zakończona', data.message || 'Przetworzono artykuły');
} else {
showToast('error', 'Błąd ekstrakcji', data.error || 'Nieznany błąd');
}
loadStats();
} catch (error) {
showToast('error', 'Błąd połączenia', error.message);
}
}
async function generateEmbeddings() {
const confirmed = await showConfirm('Generowanie embeddingów', 'Wygenerować embeddingi dla chunków bez wektorów?', '🧲');
if (!confirmed) return;
try {
const response = await fetch('/admin/zopk/knowledge/embeddings', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() }}'
},
body: JSON.stringify({ limit: 100 })
});
const data = await response.json();
if (data.success !== false) {
showToast('success', 'Embeddingi wygenerowane', data.message || 'Operacja zakończona');
} else {
showToast('error', 'Błąd generowania', data.error || 'Nieznany błąd');
}
loadStats();
} catch (error) {
showToast('error', 'Błąd połączenia', error.message);
}
}
async function autoVerifyEntities() {
const confirmed = await showConfirm('Auto-weryfikacja encji', 'Zweryfikować automatycznie encje z minimum 5 wzmiankami?', '✅');
if (!confirmed) return;
try {
const response = await fetch('/api/zopk/knowledge/auto-verify/entities', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() }}'
},
body: JSON.stringify({ min_mentions: 5, limit: 100 })
});
const data = await response.json();
if (data.success) {
showToast('success', 'Encje zweryfikowane', `Zweryfikowano ${data.verified_count} encji z ≥5 wzmiankami`);
loadStats();
loadVerificationStats();
} else {
showToast('error', 'Błąd weryfikacji', data.error);
}
} catch (error) {
showToast('error', 'Błąd połączenia', error.message);
}
}
async function autoVerifyFacts() {
const confirmed = await showConfirm('Auto-weryfikacja faktów', 'Zweryfikować automatycznie fakty z pewnością ≥70%?', '📌');
if (!confirmed) return;
try {
const response = await fetch('/api/zopk/knowledge/auto-verify/facts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() }}'
},
body: JSON.stringify({ min_importance: 0.7, limit: 200 })
});
const data = await response.json();
if (data.success) {
showToast('success', 'Fakty zweryfikowane', `Zweryfikowano ${data.verified_count} faktów z pewnością ≥70%`);
loadStats();
loadVerificationStats();
} else {
showToast('error', 'Błąd weryfikacji', data.error);
}
} catch (error) {
showToast('error', 'Błąd połączenia', error.message);
}
}
// Track undone verifications for summary
let undoneCount = 0;
async function learnFromVerified() {
const confirmed = await showConfirm(
'Uczenie z weryfikacji',
'Szukać faktów podobnych do już zweryfikowanych?<br><br>System znajdzie fakty z ≥80% podobieństwem tekstowym do zweryfikowanych wzorców i automatycznie je zweryfikuje.',
'🧠'
);
if (!confirmed) return;
try {
const response = await fetch('/api/zopk/knowledge/auto-verify/similar', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() }}'
},
body: JSON.stringify({ min_similarity: 0.8, limit: 100 })
});
const data = await response.json();
if (data.success) {
if (data.verified_count > 0) {
// Show modal with results
showSuggestionsModal(data);
} else {
showToast('warning', 'Brak nowych faktów',
'Nie znaleziono podobnych faktów do weryfikacji. Zweryfikuj więcej faktów ręcznie aby stworzyć wzorce.', 8000);
}
loadStats();
loadVerificationStats();
} else {
showToast('error', 'Błąd uczenia', data.error);
}
} catch (error) {
showToast('error', 'Błąd połączenia', error.message);
}
}
async function loadVerificationStats() {
try {
const response = await fetch('/api/zopk/knowledge/dashboard-stats');
const data = await response.json();
if (data.success) {
renderVerificationStats(data);
}
} catch (error) {
console.error('Error loading verification stats:', error);
}
}
function renderVerificationStats(data) {
// API returns: data.entities, data.facts, data.chunks (each with .total, .verified)
const entities = data.entities || {};
const facts = data.facts || {};
const chunks = data.chunks || {};
// Relations not yet implemented in API
const relations = {total: 0, verified: 0};
const html = `
<div class="stat-card" style="border-left: 3px solid #10b981;">
<div class="stat-icon">🏢</div>
<div class="stat-value">${entities.verified || 0}/${entities.total || 0}</div>
<div class="stat-label">Encje zweryfikowane</div>
<div class="stat-sublabel">${(entities.total || 0) - (entities.verified || 0)} oczekuje</div>
</div>
<div class="stat-card" style="border-left: 3px solid #3b82f6;">
<div class="stat-icon">📌</div>
<div class="stat-value">${facts.verified || 0}/${facts.total || 0}</div>
<div class="stat-label">Fakty zweryfikowane</div>
<div class="stat-sublabel">${(facts.total || 0) - (facts.verified || 0)} oczekuje</div>
</div>
<div class="stat-card" style="border-left: 3px solid #8b5cf6;">
<div class="stat-icon">📄</div>
<div class="stat-value">${chunks.verified || 0}/${chunks.total || 0}</div>
<div class="stat-label">Chunks zweryfikowane</div>
<div class="stat-sublabel">${(chunks.total || 0) - (chunks.verified || 0)} oczekuje</div>
</div>
<div class="stat-card" style="border-left: 3px solid #f59e0b;">
<div class="stat-icon">🔗</div>
<div class="stat-value">${relations.verified || 0}/${relations.total || 0}</div>
<div class="stat-label">Relacje zweryfikowane</div>
<div class="stat-sublabel">${(relations.total || 0) - (relations.verified || 0)} oczekuje</div>
</div>
`;
document.getElementById('verificationStats').innerHTML = html;
}
// Load verification stats on page load
document.addEventListener('DOMContentLoaded', function() {
loadVerificationStats();
});
// ===== Suggestions Modal Functions =====
function showSuggestionsModal(data) {
undoneCount = 0;
document.getElementById('suggestionsTotal').textContent = data.verified_count;
document.getElementById('suggestionsPatterns').textContent = data.learned_from;
document.getElementById('suggestionsUndone').textContent = '0';
const listHtml = data.verified_facts.map(fact => `
<div class="suggestion-card" id="suggestion-${fact.fact_id}" data-fact-id="${fact.fact_id}">
<div class="suggestion-header">
<span class="suggestion-type">${fact.fact_type || 'fakt'}</span>
<span class="suggestion-similarity">${fact.similarity}% podobieństwa</span>
</div>
<div class="suggestion-text">${escapeHtml(fact.fact_text)}</div>
<div class="suggestion-match">
<strong>Wzorzec:</strong> ${escapeHtml(fact.pattern_text?.substring(0, 150) || '')}${fact.pattern_text?.length > 150 ? '...' : ''}
</div>
<div class="suggestion-actions">
<button class="btn-reject" onclick="undoVerification(${fact.fact_id})">↩️ Cofnij weryfikację</button>
</div>
</div>
`).join('');
document.getElementById('suggestionsList').innerHTML = listHtml || '<div class="empty-suggestions"><div class="empty-suggestions-icon"></div><p>Wszystkie fakty zostały zweryfikowane</p></div>';
document.getElementById('suggestionsModal').style.display = 'flex';
showToast('success', 'Uczenie zakończone!', `Zweryfikowano ${data.verified_count} faktów. Przejrzyj wyniki i cofnij błędne.`, 5000);
}
function closeSuggestionsModal() {
document.getElementById('suggestionsModal').style.display = 'none';
loadStats();
loadVerificationStats();
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text || '';
return div.innerHTML;
}
async function undoVerification(factId) {
try {
const response = await fetch(`/api/zopk/knowledge/facts/${factId}/verify`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() }}'
},
body: JSON.stringify({ is_verified: false })
});
const data = await response.json();
if (data.success) {
const card = document.getElementById(`suggestion-${factId}`);
if (card) {
card.classList.add('rejected');
card.querySelector('.suggestion-actions').innerHTML = '<span style="color: var(--text-tertiary); font-size: var(--font-size-sm);">✓ Weryfikacja cofnięta</span>';
}
undoneCount++;
document.getElementById('suggestionsUndone').textContent = undoneCount;
showToast('info', 'Cofnięto weryfikację', 'Fakt został oznaczony jako niezweryfikowany');
} else {
showToast('error', 'Błąd', data.error);
}
} catch (error) {
showToast('error', 'Błąd połączenia', error.message);
}
}
// Close modal on overlay click
document.getElementById('suggestionsModal')?.addEventListener('click', function(e) {
if (e.target === this) {
closeSuggestionsModal();
}
});
{% endblock %}