feat(admin): Sekcja Baza Wiedzy AI w panelu ZOPK

- Dodano sekcję "🧠 Baza Wiedzy AI" do /admin/zopk
- Statystyki scrapingu: zatwierdzonych, zescrapowanych, oczekujących
- Statystyki wiedzy: chunks, fakty, encje
- Statystyki embeddingów: z/bez embeddingów
- Przyciski: Scrapuj treść, Ekstrakcja AI, Generuj embeddingi
- Top encje z kolorowymi pillami (firmy, osoby, miejsca, projekty)
- Rozszerzono get_extraction_statistics() o pending_scrape i embeddings

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-01-16 22:17:44 +01:00
parent 3e90cdbfc7
commit 31d5a112b8
2 changed files with 330 additions and 10 deletions

View File

@ -1263,6 +1263,92 @@
{% endif %}
</div>
<!-- AI Knowledge Base -->
<div class="panel-section">
<h2>🧠 Baza Wiedzy AI</h2>
<div id="knowledge-stats-loading" class="text-center py-4">
<span class="spinner"></span> Ładowanie statystyk...
</div>
<div id="knowledge-stats-content" style="display: none;">
<!-- Content Scraping Stats -->
<div class="stats-section">
<h3 class="stats-section-title">Scraping treści</h3>
<div class="stats-grid stats-grid-small">
<div class="stat-card info-only">
<div class="stat-value" id="kb-total-approved">-</div>
<div class="stat-label">Zatwierdzonych newsów</div>
</div>
<div class="stat-card info-only success">
<div class="stat-value" id="kb-scraped">-</div>
<div class="stat-label">Zescrapowanych</div>
</div>
<div class="stat-card info-only warning">
<div class="stat-value" id="kb-pending-scrape">-</div>
<div class="stat-label">Oczekuje na scraping</div>
</div>
<button class="stat-card filter-card ai-action" onclick="scrapeContent()" id="scrapeBtn" title="Scrapuj treść artykułów">
<div class="stat-value" style="font-size: var(--font-size-xl);">📄</div>
<div class="stat-label">Scrapuj treść</div>
</button>
</div>
</div>
<!-- Knowledge Extraction Stats -->
<div class="stats-section">
<h3 class="stats-section-title">Ekstrakcja wiedzy (Gemini AI)</h3>
<div class="stats-grid stats-grid-small">
<div class="stat-card info-only">
<div class="stat-value" id="kb-total-chunks">-</div>
<div class="stat-label">Chunks</div>
</div>
<div class="stat-card info-only">
<div class="stat-value" id="kb-total-facts">-</div>
<div class="stat-label">Faktów</div>
</div>
<div class="stat-card info-only">
<div class="stat-value" id="kb-total-entities">-</div>
<div class="stat-label">Encji</div>
</div>
<button class="stat-card filter-card ai-action" onclick="extractKnowledge()" id="extractBtn" title="Wyekstrahuj wiedzę z zescrapowanych artykułów">
<div class="stat-value" style="font-size: var(--font-size-xl);">🤖</div>
<div class="stat-label">Ekstrakcja AI</div>
</button>
</div>
</div>
<!-- Embeddings Stats -->
<div class="stats-section">
<h3 class="stats-section-title">Semantic Search (Embeddings)</h3>
<div class="stats-grid stats-grid-small">
<div class="stat-card info-only success">
<div class="stat-value" id="kb-with-embeddings">-</div>
<div class="stat-label">Z embeddingami</div>
</div>
<div class="stat-card info-only warning">
<div class="stat-value" id="kb-pending-embeddings">-</div>
<div class="stat-label">Bez embeddingów</div>
</div>
<button class="stat-card filter-card ai-action" onclick="generateEmbeddings()" id="embeddingsBtn" title="Wygeneruj embeddingi dla semantic search">
<div class="stat-value" style="font-size: var(--font-size-xl);">🔍</div>
<div class="stat-label">Generuj embeddingi</div>
</button>
</div>
</div>
<!-- Top Entities -->
<div class="stats-section" id="top-entities-section" style="display: none;">
<h3 class="stats-section-title">Najczęściej wymieniane encje</h3>
<div id="top-entities-list" class="entity-pills"></div>
</div>
</div>
<div id="knowledge-stats-error" style="display: none;" class="alert alert-danger">
Błąd ładowania statystyk
</div>
</div>
<!-- Projects -->
<div class="panel-section">
<h2>Projekty strategiczne</h2>
@ -1447,6 +1533,60 @@
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
/* Spinner */
.spinner, .spinner-small {
display: inline-block;
border: 3px solid var(--border);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
.spinner {
width: 24px;
height: 24px;
}
.spinner-small {
width: 16px;
height: 16px;
border-width: 2px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Entity Pills */
.entity-pills {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-sm);
}
.entity-pill {
display: inline-flex;
align-items: center;
gap: var(--spacing-xs);
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: var(--radius-full, 9999px);
font-size: var(--font-size-sm);
background: var(--surface-secondary, #f5f5f5);
border: 1px solid var(--border);
}
.entity-pill small {
color: var(--text-muted);
}
.entity-pill.entity-company { background: #dbeafe; border-color: #93c5fd; }
.entity-pill.entity-person { background: #fef3c7; border-color: #fcd34d; }
.entity-pill.entity-place { background: #d1fae5; border-color: #6ee7b7; }
.entity-pill.entity-organization { background: #e0e7ff; border-color: #a5b4fc; }
.entity-pill.entity-project { background: #fce7f3; border-color: #f9a8d4; }
/* Text utilities */
.text-center { text-align: center; }
.py-4 { padding-top: 1rem; padding-bottom: 1rem; }
/* Alert */
.alert { padding: var(--spacing-md); border-radius: var(--radius); }
.alert-danger { background: #fef2f2; border: 1px solid #fecaca; color: #dc2626; }
</style>
{% endblock %}
@ -2357,4 +2497,168 @@ document.getElementById('addNewsModal').addEventListener('click', function(e) {
closeAddNewsModal();
}
});
// ===========================================
// AI Knowledge Base Functions
// ===========================================
async function loadKnowledgeStats() {
try {
const response = await fetch('/admin/zopk/knowledge/stats');
const data = await response.json();
if (data.success) {
document.getElementById('knowledge-stats-loading').style.display = 'none';
document.getElementById('knowledge-stats-content').style.display = 'block';
// Scraping stats
document.getElementById('kb-total-approved').textContent = data.articles?.total_approved || 0;
document.getElementById('kb-scraped').textContent = data.articles?.scraped || 0;
document.getElementById('kb-pending-scrape').textContent = data.articles?.pending_scrape || 0;
// Knowledge base stats
document.getElementById('kb-total-chunks').textContent = data.knowledge_base?.total_chunks || 0;
document.getElementById('kb-total-facts').textContent = data.knowledge_base?.total_facts || 0;
document.getElementById('kb-total-entities').textContent = data.knowledge_base?.total_entities || 0;
// Embeddings stats
document.getElementById('kb-with-embeddings').textContent = data.knowledge_base?.chunks_with_embeddings || 0;
document.getElementById('kb-pending-embeddings').textContent = data.knowledge_base?.chunks_without_embeddings || 0;
// Top entities
if (data.top_entities && data.top_entities.length > 0) {
document.getElementById('top-entities-section').style.display = 'block';
const list = document.getElementById('top-entities-list');
list.innerHTML = data.top_entities.map(e =>
`<span class="entity-pill entity-${e.type}">${e.name} <small>(${e.mentions})</small></span>`
).join('');
}
} else {
throw new Error(data.error || 'Unknown error');
}
} catch (error) {
console.error('Error loading knowledge stats:', error);
document.getElementById('knowledge-stats-loading').style.display = 'none';
document.getElementById('knowledge-stats-error').style.display = 'block';
document.getElementById('knowledge-stats-error').textContent = `Błąd: ${error.message}`;
}
}
async function scrapeContent() {
const btn = document.getElementById('scrapeBtn');
const originalContent = btn.innerHTML;
const confirmed = await showConfirm(
'Czy chcesz rozpocząć scrapowanie treści artykułów?<br><br>' +
'<small>Proces pobierze pełną treść z zatwierdzonych newsów które jeszcze nie mają treści.</small>',
{ icon: '📄', title: 'Scraping treści', okText: 'Rozpocznij', okClass: 'btn-primary' }
);
if (!confirmed) return;
btn.disabled = true;
btn.innerHTML = '<span class="spinner-small"></span>';
try {
const response = await fetch('/admin/zopk/news/scrape-content', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken },
body: JSON.stringify({ limit: 30 })
});
const data = await response.json();
if (data.success) {
showToast(data.message || `Zescrapowano ${data.scraped || 0} artykułów`, 'success');
loadKnowledgeStats();
} else {
showToast(data.error || 'Błąd scrapowania', 'error');
}
} catch (error) {
showToast(`Błąd: ${error.message}`, 'error');
} finally {
btn.disabled = false;
btn.innerHTML = originalContent;
}
}
async function extractKnowledge() {
const btn = document.getElementById('extractBtn');
const originalContent = btn.innerHTML;
const confirmed = await showConfirm(
'Czy chcesz uruchomić ekstrakcję wiedzy przez AI?<br><br>' +
'<small>Gemini AI przeanalizuje zescrapowane artykuły i wyekstrahuje:<br>' +
'• Chunks (fragmenty tekstu)<br>' +
'• Fakty (daty, liczby, decyzje)<br>' +
'• Encje (firmy, osoby, projekty)</small>',
{ icon: '🤖', title: 'Ekstrakcja wiedzy', okText: 'Uruchom AI', okClass: 'btn-primary' }
);
if (!confirmed) return;
btn.disabled = true;
btn.innerHTML = '<span class="spinner-small"></span>';
try {
const response = await fetch('/admin/zopk/knowledge/extract', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken },
body: JSON.stringify({ limit: 10 })
});
const data = await response.json();
if (data.success) {
showToast(data.message || 'Ekstrakcja zakończona', 'success');
loadKnowledgeStats();
} else {
showToast(data.error || 'Błąd ekstrakcji', 'error');
}
} catch (error) {
showToast(`Błąd: ${error.message}`, 'error');
} finally {
btn.disabled = false;
btn.innerHTML = originalContent;
}
}
async function generateEmbeddings() {
const btn = document.getElementById('embeddingsBtn');
const originalContent = btn.innerHTML;
const confirmed = await showConfirm(
'Czy chcesz wygenerować embeddingi dla semantic search?<br><br>' +
'<small>Google Text Embedding API przekształci tekst w wektory 768-wymiarowe.<br>' +
'Embeddingi umożliwiają inteligentne wyszukiwanie w bazie wiedzy.</small>',
{ icon: '🔍', title: 'Generowanie embeddingów', okText: 'Generuj', okClass: 'btn-primary' }
);
if (!confirmed) return;
btn.disabled = true;
btn.innerHTML = '<span class="spinner-small"></span>';
try {
const response = await fetch('/admin/zopk/knowledge/embeddings', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken },
body: JSON.stringify({ limit: 50 })
});
const data = await response.json();
if (data.success) {
showToast(data.message || 'Embeddingi wygenerowane', 'success');
loadKnowledgeStats();
} else {
showToast(data.error || 'Błąd generowania', 'error');
}
} catch (error) {
showToast(`Błąd: ${error.message}`, 'error');
} finally {
btn.disabled = false;
btn.innerHTML = originalContent;
}
}
// Load knowledge stats on page load
document.addEventListener('DOMContentLoaded', loadKnowledgeStats);
{% endblock %}

View File

@ -740,20 +740,33 @@ class ZOPKKnowledgeService:
ZOPKNews.scrape_status == 'scraped'
).scalar()
# Pending scrape: approved but not yet scraped
pending_scrape = self.db.query(func.count(ZOPKNews.id)).filter(
ZOPKNews.status.in_(['approved', 'auto_approved']),
ZOPKNews.scrape_status.in_(['pending', None])
).scalar()
extracted = self.db.query(func.count(ZOPKNews.id)).filter(
ZOPKNews.knowledge_extracted == True
).scalar()
pending = self.db.query(func.count(ZOPKNews.id)).filter(
pending_extract = self.db.query(func.count(ZOPKNews.id)).filter(
ZOPKNews.scrape_status == 'scraped',
ZOPKNews.knowledge_extracted == False
).scalar()
# Knowledge base stats
chunks_count = self.db.query(func.count(ZOPKKnowledgeChunk.id)).scalar()
facts_count = self.db.query(func.count(ZOPKKnowledgeFact.id)).scalar()
entities_count = self.db.query(func.count(ZOPKKnowledgeEntity.id)).scalar()
relations_count = self.db.query(func.count(ZOPKKnowledgeRelation.id)).scalar()
total_chunks = self.db.query(func.count(ZOPKKnowledgeChunk.id)).scalar()
total_facts = self.db.query(func.count(ZOPKKnowledgeFact.id)).scalar()
total_entities = self.db.query(func.count(ZOPKKnowledgeEntity.id)).scalar()
total_relations = self.db.query(func.count(ZOPKKnowledgeRelation.id)).scalar()
# Embeddings stats
chunks_with_embeddings = self.db.query(func.count(ZOPKKnowledgeChunk.id)).filter(
ZOPKKnowledgeChunk.embedding.isnot(None)
).scalar()
chunks_without_embeddings = (total_chunks or 0) - (chunks_with_embeddings or 0)
# Top entities by mentions
top_entities = self.db.query(
@ -768,14 +781,17 @@ class ZOPKKnowledgeService:
'articles': {
'total_approved': total_approved or 0,
'scraped': scraped or 0,
'pending_scrape': pending_scrape or 0,
'extracted': extracted or 0,
'pending': pending or 0
'pending_extract': pending_extract or 0
},
'knowledge_base': {
'chunks': chunks_count or 0,
'facts': facts_count or 0,
'entities': entities_count or 0,
'relations': relations_count or 0
'total_chunks': total_chunks or 0,
'total_facts': total_facts or 0,
'total_entities': total_entities or 0,
'total_relations': total_relations or 0,
'chunks_with_embeddings': chunks_with_embeddings or 0,
'chunks_without_embeddings': chunks_without_embeddings or 0
},
'top_entities': [
{'name': e[0], 'type': e[1], 'mentions': e[2]}