nordabiz/templates/admin/zopk_knowledge_dashboard.html
Maciej Pienczyn 96fa0058c2 feat(zopk): Rozbudowa bazy wiedzy ZOPK
- Dodano skrypt cron do automatycznej ekstrakcji wiedzy (scripts/cron_extract_knowledge.py)
- Dodano panel deduplikacji faktów (/admin/zopk/knowledge/fact-duplicates)
- Dodano API i funkcje auto-weryfikacji encji i faktów
- Dodano panel Timeline ZOPK (/admin/zopk/timeline) z CRUD
- Rozszerzono dashboard bazy wiedzy o statystyki weryfikacji i przyciski auto-weryfikacji
- Dodano migrację 016_zopk_milestones.sql dla tabeli kamieni milowych
- Naprawiono duplikat modelu ZOPKMilestone w database.py

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 10:57:11 +01:00

677 lines
22 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

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);
}
</style>
{% endblock %}
{% block content %}
<div class="container">
<div class="breadcrumb">
<a href="{{ url_for('admin_zopk') }}">Panel Admina</a>
<span></span>
<a href="{{ url_for('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_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_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_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_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_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_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_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_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>
<a href="{{ url_for('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 %}
// Load stats on 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_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_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_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() {
if (!confirm('Uruchomić ekstrakcję wiedzy z artykułów?')) 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();
alert(data.message || data.error);
loadStats();
} catch (error) {
alert('Błąd: ' + error.message);
}
}
async function generateEmbeddings() {
if (!confirm('Wygenerować embeddingi dla chunków?')) 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();
alert(data.message || data.error);
loadStats();
} catch (error) {
alert('Błąd: ' + error.message);
}
}
async function autoVerifyEntities() {
if (!confirm('Auto-weryfikować encje z ≥5 wzmiankami?')) 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) {
alert(`✅ Zweryfikowano ${data.verified_count} encji`);
loadStats();
loadVerificationStats();
} else {
alert('Błąd: ' + data.error);
}
} catch (error) {
alert('Błąd: ' + error.message);
}
}
async function autoVerifyFacts() {
if (!confirm('Auto-weryfikować fakty z ważnością ≥70%?')) 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) {
alert(`✅ Zweryfikowano ${data.verified_count} faktów`);
loadStats();
loadVerificationStats();
} else {
alert('Błąd: ' + data.error);
}
} catch (error) {
alert('Błąd: ' + 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) {
const stats = data.verification || {};
const html = `
<div class="stat-card" style="border-left: 3px solid #10b981;">
<div class="stat-icon">🏢</div>
<div class="stat-value">${stats.entities_verified || 0}/${stats.entities_total || 0}</div>
<div class="stat-label">Encje zweryfikowane</div>
<div class="stat-sublabel">${stats.entities_pending || 0} oczekuje</div>
</div>
<div class="stat-card" style="border-left: 3px solid #3b82f6;">
<div class="stat-icon">📌</div>
<div class="stat-value">${stats.facts_verified || 0}/${stats.facts_total || 0}</div>
<div class="stat-label">Fakty zweryfikowane</div>
<div class="stat-sublabel">${stats.facts_pending || 0} oczekuje</div>
</div>
<div class="stat-card" style="border-left: 3px solid #8b5cf6;">
<div class="stat-icon">📄</div>
<div class="stat-value">${stats.chunks_verified || 0}/${stats.chunks_total || 0}</div>
<div class="stat-label">Chunks zweryfikowane</div>
<div class="stat-sublabel">${stats.chunks_pending || 0} oczekuje</div>
</div>
<div class="stat-card" style="border-left: 3px solid #f59e0b;">
<div class="stat-icon">🔗</div>
<div class="stat-value">${stats.relations_verified || 0}/${stats.relations_total || 0}</div>
<div class="stat-label">Relacje zweryfikowane</div>
<div class="stat-sublabel">${stats.relations_pending || 0} oczekuje</div>
</div>
`;
document.getElementById('verificationStats').innerHTML = html;
}
// Load verification stats on page load
document.addEventListener('DOMContentLoaded', function() {
loadVerificationStats();
});
{% endblock %}