Zmieniono admin_dashboard i admin_zopk_dashboard na admin_zopk Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
645 lines
20 KiB
HTML
645 lines
20 KiB
HTML
{% extends "base.html" %}
|
||
|
||
{% block title %}Chunks - Baza Wiedzy ZOPK{% 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);
|
||
}
|
||
|
||
.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);
|
||
}
|
||
|
||
.filters {
|
||
display: flex;
|
||
gap: var(--spacing-md);
|
||
margin-bottom: var(--spacing-lg);
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
background: var(--surface);
|
||
padding: var(--spacing-md);
|
||
border-radius: var(--radius);
|
||
}
|
||
|
||
.filter-btn {
|
||
padding: var(--spacing-sm) var(--spacing-md);
|
||
border-radius: var(--radius);
|
||
border: 1px solid var(--border);
|
||
background: var(--surface);
|
||
text-decoration: none;
|
||
color: var(--text-secondary);
|
||
font-size: var(--font-size-sm);
|
||
transition: var(--transition);
|
||
cursor: pointer;
|
||
}
|
||
|
||
.filter-btn:hover {
|
||
background: var(--background);
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.filter-btn.active {
|
||
background: var(--primary);
|
||
border-color: var(--primary);
|
||
color: white;
|
||
}
|
||
|
||
.table-wrapper {
|
||
overflow-x: auto;
|
||
background: var(--surface);
|
||
border-radius: var(--radius-lg);
|
||
box-shadow: var(--shadow);
|
||
}
|
||
|
||
.data-table {
|
||
width: 100%;
|
||
min-width: 800px;
|
||
}
|
||
|
||
.data-table th,
|
||
.data-table td {
|
||
padding: var(--spacing-md);
|
||
text-align: left;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
|
||
.data-table th {
|
||
background: var(--background);
|
||
font-weight: 600;
|
||
font-size: var(--font-size-sm);
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.data-table tr:hover {
|
||
background: var(--background);
|
||
}
|
||
|
||
.chunk-content {
|
||
max-width: 400px;
|
||
font-size: var(--font-size-sm);
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.chunk-summary {
|
||
color: var(--text-secondary);
|
||
font-style: italic;
|
||
font-size: var(--font-size-xs);
|
||
margin-top: var(--spacing-xs);
|
||
}
|
||
|
||
.chunk-source {
|
||
font-size: var(--font-size-xs);
|
||
color: var(--text-tertiary);
|
||
}
|
||
|
||
.chunk-source a {
|
||
color: var(--primary);
|
||
text-decoration: none;
|
||
}
|
||
|
||
.chunk-source a:hover {
|
||
text-decoration: underline;
|
||
}
|
||
|
||
.status-badge {
|
||
display: inline-block;
|
||
padding: 2px 8px;
|
||
border-radius: var(--radius-sm);
|
||
font-size: var(--font-size-xs);
|
||
font-weight: 500;
|
||
}
|
||
|
||
.status-verified { background: #d1fae5; color: #065f46; }
|
||
.status-pending { background: #fef3c7; color: #92400e; }
|
||
.status-embedding { background: #dbeafe; color: #1e40af; }
|
||
.status-no-embedding { background: #fee2e2; color: #991b1b; }
|
||
|
||
.importance-stars {
|
||
color: #fbbf24;
|
||
font-size: var(--font-size-sm);
|
||
}
|
||
|
||
.action-btn {
|
||
padding: var(--spacing-xs) var(--spacing-sm);
|
||
border-radius: var(--radius-sm);
|
||
border: none;
|
||
cursor: pointer;
|
||
font-size: var(--font-size-xs);
|
||
transition: var(--transition);
|
||
margin-right: var(--spacing-xs);
|
||
}
|
||
|
||
.action-btn-view {
|
||
background: var(--background);
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.action-btn-view:hover {
|
||
background: var(--primary);
|
||
color: white;
|
||
}
|
||
|
||
.action-btn-verify {
|
||
background: #d1fae5;
|
||
color: #065f46;
|
||
}
|
||
|
||
.action-btn-verify:hover {
|
||
background: #065f46;
|
||
color: white;
|
||
}
|
||
|
||
.action-btn-delete {
|
||
background: #fee2e2;
|
||
color: #991b1b;
|
||
}
|
||
|
||
.action-btn-delete:hover {
|
||
background: #991b1b;
|
||
color: white;
|
||
}
|
||
|
||
.pagination {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: var(--spacing-xs);
|
||
padding: var(--spacing-lg);
|
||
}
|
||
|
||
.pagination a,
|
||
.pagination span {
|
||
padding: var(--spacing-sm) var(--spacing-md);
|
||
border-radius: var(--radius);
|
||
text-decoration: none;
|
||
font-size: var(--font-size-sm);
|
||
border: 1px solid var(--border);
|
||
background: var(--surface);
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.pagination a:hover {
|
||
background: var(--primary);
|
||
border-color: var(--primary);
|
||
color: white;
|
||
}
|
||
|
||
.pagination .current {
|
||
background: var(--primary);
|
||
border-color: var(--primary);
|
||
color: white;
|
||
}
|
||
|
||
.pagination .disabled {
|
||
opacity: 0.5;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.stats-bar {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: var(--spacing-md);
|
||
background: var(--background);
|
||
border-radius: var(--radius);
|
||
margin-bottom: var(--spacing-md);
|
||
font-size: var(--font-size-sm);
|
||
}
|
||
|
||
/* Modal */
|
||
.modal-overlay {
|
||
display: none;
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0,0,0,0.5);
|
||
z-index: 1000;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
|
||
.modal-overlay.active {
|
||
display: flex;
|
||
}
|
||
|
||
.modal {
|
||
background: var(--surface);
|
||
border-radius: var(--radius-lg);
|
||
max-width: 700px;
|
||
width: 90%;
|
||
max-height: 80vh;
|
||
overflow-y: auto;
|
||
padding: var(--spacing-xl);
|
||
}
|
||
|
||
.modal-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: var(--spacing-lg);
|
||
}
|
||
|
||
.modal-title {
|
||
font-size: var(--font-size-lg);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.modal-close {
|
||
background: none;
|
||
border: none;
|
||
font-size: 1.5rem;
|
||
cursor: pointer;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.modal-section {
|
||
margin-bottom: var(--spacing-lg);
|
||
}
|
||
|
||
.modal-section-title {
|
||
font-size: var(--font-size-sm);
|
||
font-weight: 600;
|
||
color: var(--text-secondary);
|
||
margin-bottom: var(--spacing-sm);
|
||
}
|
||
|
||
.modal-content-box {
|
||
background: var(--background);
|
||
padding: var(--spacing-md);
|
||
border-radius: var(--radius);
|
||
font-size: var(--font-size-sm);
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.keyword-tag {
|
||
display: inline-block;
|
||
padding: 2px 8px;
|
||
background: var(--primary);
|
||
color: white;
|
||
border-radius: var(--radius-sm);
|
||
font-size: var(--font-size-xs);
|
||
margin: 2px;
|
||
}
|
||
|
||
.fact-item, .entity-item {
|
||
padding: var(--spacing-sm);
|
||
background: var(--background);
|
||
border-radius: var(--radius);
|
||
margin-bottom: var(--spacing-xs);
|
||
font-size: var(--font-size-sm);
|
||
}
|
||
</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>
|
||
<a href="{{ url_for('admin_zopk_knowledge_dashboard') }}">Baza Wiedzy</a>
|
||
<span>›</span>
|
||
<span>Chunks</span>
|
||
</div>
|
||
|
||
<div class="page-header">
|
||
<h1>📄 Chunks (Fragmenty tekstu)</h1>
|
||
</div>
|
||
|
||
<!-- Filters -->
|
||
<div class="filters">
|
||
<span style="color: var(--text-secondary); font-size: var(--font-size-sm);">Filtruj:</span>
|
||
<a href="{{ url_for('admin_zopk_knowledge_chunks') }}"
|
||
class="filter-btn {{ 'active' if has_embedding is none and is_verified is none else '' }}">
|
||
Wszystkie
|
||
</a>
|
||
<a href="{{ url_for('admin_zopk_knowledge_chunks', has_embedding='true') }}"
|
||
class="filter-btn {{ 'active' if has_embedding == true else '' }}">
|
||
✓ Z embeddingiem
|
||
</a>
|
||
<a href="{{ url_for('admin_zopk_knowledge_chunks', has_embedding='false') }}"
|
||
class="filter-btn {{ 'active' if has_embedding == false else '' }}">
|
||
✗ Bez embeddingu
|
||
</a>
|
||
<a href="{{ url_for('admin_zopk_knowledge_chunks', is_verified='true') }}"
|
||
class="filter-btn {{ 'active' if is_verified == true else '' }}">
|
||
✓ Zweryfikowane
|
||
</a>
|
||
<a href="{{ url_for('admin_zopk_knowledge_chunks', is_verified='false') }}"
|
||
class="filter-btn {{ 'active' if is_verified == false else '' }}">
|
||
⏳ Oczekujące
|
||
</a>
|
||
</div>
|
||
|
||
<!-- Stats Bar -->
|
||
<div class="stats-bar">
|
||
<span>Pokazuję {{ chunks|length }} z {{ total }} chunków (strona {{ page }} z {{ pages }})</span>
|
||
<span>
|
||
{% if source_news_id %}
|
||
Źródło: Artykuł #{{ source_news_id }}
|
||
<a href="{{ url_for('admin_zopk_knowledge_chunks') }}" style="margin-left: 8px; color: var(--primary);">✕ usuń filtr</a>
|
||
{% endif %}
|
||
</span>
|
||
</div>
|
||
|
||
<!-- Table -->
|
||
<div class="table-wrapper">
|
||
<table class="data-table">
|
||
<thead>
|
||
<tr>
|
||
<th>ID</th>
|
||
<th>Treść</th>
|
||
<th>Typ</th>
|
||
<th>Ważność</th>
|
||
<th>Status</th>
|
||
<th>Źródło</th>
|
||
<th>Akcje</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for chunk in chunks %}
|
||
<tr id="chunk-row-{{ chunk.id }}">
|
||
<td>#{{ chunk.id }}</td>
|
||
<td class="chunk-content">
|
||
{{ chunk.content }}
|
||
{% if chunk.summary %}
|
||
<div class="chunk-summary">{{ chunk.summary }}</div>
|
||
{% endif %}
|
||
</td>
|
||
<td>
|
||
{% if chunk.chunk_type %}
|
||
<span class="status-badge">{{ chunk.chunk_type }}</span>
|
||
{% else %}
|
||
<span style="color: var(--text-tertiary);">—</span>
|
||
{% endif %}
|
||
</td>
|
||
<td>
|
||
{% if chunk.importance_score %}
|
||
<span class="importance-stars">
|
||
{% for i in range(chunk.importance_score) %}★{% endfor %}{% for i in range(5 - chunk.importance_score) %}☆{% endfor %}
|
||
</span>
|
||
{% else %}
|
||
<span style="color: var(--text-tertiary);">—</span>
|
||
{% endif %}
|
||
</td>
|
||
<td>
|
||
{% if chunk.is_verified %}
|
||
<span class="status-badge status-verified">✓ Zweryfikowany</span>
|
||
{% else %}
|
||
<span class="status-badge status-pending">⏳ Oczekuje</span>
|
||
{% endif %}
|
||
<br>
|
||
{% if chunk.has_embedding %}
|
||
<span class="status-badge status-embedding">🧲 Embedding</span>
|
||
{% else %}
|
||
<span class="status-badge status-no-embedding">✗ Brak</span>
|
||
{% endif %}
|
||
</td>
|
||
<td class="chunk-source">
|
||
{% if chunk.source_news_id %}
|
||
<a href="{{ url_for('admin_zopk_knowledge_chunks', source_news_id=chunk.source_news_id) }}">
|
||
Artykuł #{{ chunk.source_news_id }}
|
||
</a>
|
||
{% if chunk.source_title %}
|
||
<br>{{ chunk.source_title[:50] }}...
|
||
{% endif %}
|
||
{% endif %}
|
||
</td>
|
||
<td>
|
||
<button class="action-btn action-btn-view" onclick="viewChunk({{ chunk.id }})">👁️</button>
|
||
<button class="action-btn action-btn-verify" onclick="toggleVerify({{ chunk.id }}, {{ 'false' if chunk.is_verified else 'true' }})">
|
||
{{ '✗' if chunk.is_verified else '✓' }}
|
||
</button>
|
||
<button class="action-btn action-btn-delete" onclick="deleteChunk({{ chunk.id }})">🗑️</button>
|
||
</td>
|
||
</tr>
|
||
{% else %}
|
||
<tr>
|
||
<td colspan="7" style="text-align: center; padding: var(--spacing-xl); color: var(--text-secondary);">
|
||
Brak chunków do wyświetlenia
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- Pagination -->
|
||
{% if pages > 1 %}
|
||
<div class="pagination">
|
||
{% if page > 1 %}
|
||
<a href="{{ url_for('admin_zopk_knowledge_chunks', page=page-1, has_embedding=has_embedding, is_verified=is_verified, source_news_id=source_news_id) }}">‹ Poprzednia</a>
|
||
{% else %}
|
||
<span class="disabled">‹ Poprzednia</span>
|
||
{% endif %}
|
||
|
||
{% for p in range(1, pages + 1) %}
|
||
{% if p == page %}
|
||
<span class="current">{{ p }}</span>
|
||
{% elif p <= 3 or p >= pages - 2 or (p >= page - 1 and p <= page + 1) %}
|
||
<a href="{{ url_for('admin_zopk_knowledge_chunks', page=p, has_embedding=has_embedding, is_verified=is_verified, source_news_id=source_news_id) }}">{{ p }}</a>
|
||
{% elif p == 4 or p == pages - 3 %}
|
||
<span>...</span>
|
||
{% endif %}
|
||
{% endfor %}
|
||
|
||
{% if page < pages %}
|
||
<a href="{{ url_for('admin_zopk_knowledge_chunks', page=page+1, has_embedding=has_embedding, is_verified=is_verified, source_news_id=source_news_id) }}">Następna ›</a>
|
||
{% else %}
|
||
<span class="disabled">Następna ›</span>
|
||
{% endif %}
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<!-- Detail Modal -->
|
||
<div class="modal-overlay" id="chunkModal">
|
||
<div class="modal">
|
||
<div class="modal-header">
|
||
<div class="modal-title">📄 Szczegóły chunka</div>
|
||
<button class="modal-close" onclick="closeModal()">×</button>
|
||
</div>
|
||
<div id="modalContent">
|
||
<div style="text-align: center; padding: 20px;">Ładowanie...</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block extra_js %}
|
||
async function viewChunk(id) {
|
||
document.getElementById('chunkModal').classList.add('active');
|
||
document.getElementById('modalContent').innerHTML = '<div style="text-align: center; padding: 20px;">Ładowanie...</div>';
|
||
|
||
try {
|
||
const response = await fetch(`/api/zopk/knowledge/chunks/${id}`);
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
renderChunkDetail(data.chunk);
|
||
} else {
|
||
document.getElementById('modalContent').innerHTML = `<div style="color: red;">Błąd: ${data.error}</div>`;
|
||
}
|
||
} catch (error) {
|
||
document.getElementById('modalContent').innerHTML = `<div style="color: red;">Błąd: ${error.message}</div>`;
|
||
}
|
||
}
|
||
|
||
function renderChunkDetail(chunk) {
|
||
let html = `
|
||
<div class="modal-section">
|
||
<div class="modal-section-title">Treść</div>
|
||
<div class="modal-content-box">${chunk.content}</div>
|
||
</div>
|
||
`;
|
||
|
||
if (chunk.summary) {
|
||
html += `
|
||
<div class="modal-section">
|
||
<div class="modal-section-title">Podsumowanie</div>
|
||
<div class="modal-content-box">${chunk.summary}</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
if (chunk.keywords && chunk.keywords.length) {
|
||
html += `
|
||
<div class="modal-section">
|
||
<div class="modal-section-title">Słowa kluczowe</div>
|
||
<div>${chunk.keywords.map(k => `<span class="keyword-tag">${k}</span>`).join('')}</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
if (chunk.facts && chunk.facts.length) {
|
||
html += `
|
||
<div class="modal-section">
|
||
<div class="modal-section-title">Fakty (${chunk.facts.length})</div>
|
||
${chunk.facts.map(f => `<div class="fact-item">${f.full_text}</div>`).join('')}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
if (chunk.entity_mentions && chunk.entity_mentions.length) {
|
||
html += `
|
||
<div class="modal-section">
|
||
<div class="modal-section-title">Encje (${chunk.entity_mentions.length})</div>
|
||
${chunk.entity_mentions.map(e => `<div class="entity-item">${e.entity_name} (${e.entity_type})</div>`).join('')}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
html += `
|
||
<div class="modal-section">
|
||
<div class="modal-section-title">Metadane</div>
|
||
<div class="modal-content-box">
|
||
<strong>Typ:</strong> ${chunk.chunk_type || 'brak'}<br>
|
||
<strong>Ważność:</strong> ${chunk.importance_score || 'brak'}/5<br>
|
||
<strong>Pewność:</strong> ${chunk.confidence_score ? (chunk.confidence_score * 100).toFixed(0) + '%' : 'brak'}<br>
|
||
<strong>Tokeny:</strong> ${chunk.token_count || 'brak'}<br>
|
||
<strong>Model:</strong> ${chunk.extraction_model || 'brak'}<br>
|
||
<strong>Embedding:</strong> ${chunk.has_embedding ? 'Tak' : 'Nie'}<br>
|
||
<strong>Zweryfikowany:</strong> ${chunk.is_verified ? 'Tak' : 'Nie'}
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
if (chunk.source_news) {
|
||
html += `
|
||
<div class="modal-section">
|
||
<div class="modal-section-title">Źródło</div>
|
||
<div class="modal-content-box">
|
||
<strong>Artykuł:</strong> ${chunk.source_news.title}<br>
|
||
<strong>Portal:</strong> ${chunk.source_news.source_name || 'nieznany'}<br>
|
||
<a href="${chunk.source_news.url}" target="_blank">Otwórz źródło →</a>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
document.getElementById('modalContent').innerHTML = html;
|
||
}
|
||
|
||
function closeModal() {
|
||
document.getElementById('chunkModal').classList.remove('active');
|
||
}
|
||
|
||
document.getElementById('chunkModal').addEventListener('click', function(e) {
|
||
if (e.target === this) closeModal();
|
||
});
|
||
|
||
async function toggleVerify(id, newState) {
|
||
try {
|
||
const response = await fetch(`/api/zopk/knowledge/chunks/${id}/verify`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-CSRFToken': '{{ csrf_token() }}'
|
||
},
|
||
body: JSON.stringify({ is_verified: newState })
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
location.reload();
|
||
} else {
|
||
alert('Błąd: ' + data.error);
|
||
}
|
||
} catch (error) {
|
||
alert('Błąd: ' + error.message);
|
||
}
|
||
}
|
||
|
||
async function deleteChunk(id) {
|
||
if (!confirm('Czy na pewno usunąć ten chunk? Zostaną również usunięte powiązane fakty i wzmianki.')) return;
|
||
|
||
try {
|
||
const response = await fetch(`/api/zopk/knowledge/chunks/${id}`, {
|
||
method: 'DELETE',
|
||
headers: {
|
||
'X-CSRFToken': '{{ csrf_token() }}'
|
||
}
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
document.getElementById('chunk-row-' + id).remove();
|
||
} else {
|
||
alert('Błąd: ' + data.error);
|
||
}
|
||
} catch (error) {
|
||
alert('Błąd: ' + error.message);
|
||
}
|
||
}
|
||
{% endblock %}
|