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:
parent
3e90cdbfc7
commit
31d5a112b8
@ -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 %}
|
||||
|
||||
@ -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]}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user