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
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>
1235 lines
39 KiB
HTML
1235 lines
39 KiB
HTML
{% 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 %}
|