nordabiz/templates/admin/zopk_knowledge_dashboard.html
Maciej Pienczyn 6d1f75bce5 fix(admin): Naprawiono błędne nazwy endpointów w breadcrumbs
Zmieniono admin_dashboard i admin_zopk_dashboard na admin_zopk

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

526 lines
16 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_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>
</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>
<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>
</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);
}
}
{% endblock %}