nordabiz/templates/admin/zopk_knowledge_chunks.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

645 lines
20 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 %}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()">&times;</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 %}