nordabiz/templates/admin/zopk_fact_duplicates.html
Maciej Pienczyn 094379d95e
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
fix(templates): Add blueprint prefix to url_for calls across admin templates
After refactoring to blueprints, templates still used bare endpoint names
(e.g., url_for('admin_zopk')) instead of prefixed names (e.g.,
url_for('admin.admin_zopk')). While most worked via backward-compat aliases,
api_zopk_search_news was missing from the alias list causing 500 on /admin/zopk.

Fixed 19 template files and added missing alias for api_zopk_search_news.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 13:44:50 +01:00

218 lines
10 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 %}Deduplikacja Faktów - ZOPK{% endblock %}
{% block extra_css %}
<style>
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--spacing-lg); }
.page-header h1 { font-size: var(--font-size-2xl); color: var(--text-primary); }
.breadcrumb { display: flex; gap: var(--spacing-xs); color: var(--text-secondary); font-size: var(--font-size-sm); margin-bottom: var(--spacing-lg); }
.breadcrumb a { color: var(--primary); text-decoration: none; }
.controls { display: flex; gap: var(--spacing-md); margin-bottom: var(--spacing-lg); align-items: center; flex-wrap: wrap; }
.control-group { display: flex; align-items: center; gap: var(--spacing-xs); }
.control-group label { font-size: var(--font-size-sm); color: var(--text-secondary); }
.control-group input, .control-group select { padding: 6px 12px; border: 1px solid var(--border); border-radius: var(--radius); }
.duplicate-card { background: var(--surface); border-radius: var(--radius-lg); box-shadow: var(--shadow); margin-bottom: var(--spacing-md); overflow: hidden; }
.duplicate-header { background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); color: white; padding: var(--spacing-sm) var(--spacing-md); display: flex; justify-content: space-between; align-items: center; }
.similarity-badge { background: rgba(255,255,255,0.2); padding: 4px 12px; border-radius: 20px; font-weight: 600; }
.duplicate-content { display: grid; grid-template-columns: 1fr auto 1fr; gap: var(--spacing-md); padding: var(--spacing-md); }
.fact-box { background: var(--background); padding: var(--spacing-md); border-radius: var(--radius); }
.fact-box.primary { border-left: 4px solid var(--success); }
.fact-box.duplicate { border-left: 4px solid var(--error); }
.fact-text { font-size: var(--font-size-sm); line-height: 1.6; margin-bottom: var(--spacing-sm); }
.fact-meta { font-size: var(--font-size-xs); color: var(--text-secondary); }
.merge-arrow { display: flex; align-items: center; justify-content: center; font-size: 2rem; color: var(--primary); }
.duplicate-actions { padding: var(--spacing-sm) var(--spacing-md); background: var(--background); display: flex; justify-content: flex-end; gap: var(--spacing-sm); }
.btn { padding: 8px 16px; border-radius: var(--radius); font-weight: 500; cursor: pointer; border: none; transition: var(--transition); }
.btn-primary { background: var(--primary); color: white; }
.btn-primary:hover { background: var(--primary-dark); }
.btn-secondary { background: var(--surface); color: var(--text-primary); border: 1px solid var(--border); }
.stats-bar { display: flex; gap: var(--spacing-lg); margin-bottom: var(--spacing-lg); }
.stat-item { background: var(--surface); padding: var(--spacing-md); border-radius: var(--radius); text-align: center; }
.stat-value { font-size: var(--font-size-2xl); font-weight: 700; color: var(--primary); }
.stat-label { font-size: var(--font-size-sm); color: var(--text-secondary); }
.loading { text-align: center; padding: var(--spacing-xl); color: var(--text-secondary); }
.empty-state { text-align: center; padding: var(--spacing-xl); color: var(--text-secondary); }
</style>
{% endblock %}
{% block content %}
<div class="container">
<div class="breadcrumb">
<a href="{{ url_for('admin.admin_zopk') }}">Panel ZOPK</a>
<span></span>
<a href="{{ url_for('admin.admin_zopk_knowledge_dashboard') }}">Baza Wiedzy</a>
<span></span>
<span>Deduplikacja Faktów</span>
</div>
<div class="page-header">
<h1>🔀 Deduplikacja Faktów</h1>
</div>
<div class="controls">
<div class="control-group">
<label>Min. podobieństwo:</label>
<input type="range" id="minSimilarity" min="0.5" max="0.95" step="0.05" value="0.7" oninput="document.getElementById('simValue').textContent = this.value">
<span id="simValue">0.7</span>
</div>
<div class="control-group">
<label>Typ faktu:</label>
<select id="factType">
<option value="">Wszystkie</option>
<option value="investment">Inwestycja</option>
<option value="decision">Decyzja</option>
<option value="milestone">Kamień milowy</option>
<option value="statistic">Statystyka</option>
</select>
</div>
<button class="btn btn-primary" onclick="loadDuplicates()">🔍 Szukaj duplikatów</button>
<button class="btn btn-secondary" onclick="mergeAllHigh()">⚡ Połącz wszystkie >90%</button>
</div>
<div class="stats-bar">
<div class="stat-item">
<div class="stat-value" id="totalPairs">-</div>
<div class="stat-label">Par duplikatów</div>
</div>
<div class="stat-item">
<div class="stat-value" id="avgSimilarity">-</div>
<div class="stat-label">Śr. podobieństwo</div>
</div>
</div>
<div id="duplicatesList">
<div class="loading">Kliknij "Szukaj duplikatów" aby rozpocząć...</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
let duplicatesData = [];
async function loadDuplicates() {
const minSim = document.getElementById('minSimilarity').value;
const factType = document.getElementById('factType').value;
document.getElementById('duplicatesList').innerHTML = '<div class="loading">Ładowanie...</div>';
try {
const url = `/api/zopk/knowledge/fact-duplicates?min_similarity=${minSim}&fact_type=${factType}&limit=100`;
const response = await fetch(url);
const data = await response.json();
if (data.success) {
duplicatesData = data.duplicates;
document.getElementById('totalPairs').textContent = data.count;
if (data.count > 0) {
const avgSim = duplicatesData.reduce((sum, d) => sum + d.similarity, 0) / data.count;
document.getElementById('avgSimilarity').textContent = (avgSim * 100).toFixed(0) + '%';
}
renderDuplicates();
}
} catch (error) {
document.getElementById('duplicatesList').innerHTML = '<div class="empty-state">Błąd ładowania: ' + error + '</div>';
}
}
function renderDuplicates() {
if (duplicatesData.length === 0) {
document.getElementById('duplicatesList').innerHTML = '<div class="empty-state">Brak duplikatów do pokazania</div>';
return;
}
const html = duplicatesData.map((d, idx) => `
<div class="duplicate-card" id="dup-${idx}">
<div class="duplicate-header">
<span>${d.fact1.fact_type || 'fakt'}</span>
<span class="similarity-badge">${(d.similarity * 100).toFixed(0)}% podobieństwa</span>
</div>
<div class="duplicate-content">
<div class="fact-box primary">
<div class="fact-text">${escapeHtml(d.fact1.text)}</div>
<div class="fact-meta">
ID: ${d.fact1.id}${d.fact1.confidence_score ? ' | Pewność: ' + (d.fact1.confidence_score * 100).toFixed(0) + '%' : ''}
${d.fact1.is_verified ? ' | ✅ Zweryfikowany' : ''}
</div>
</div>
<div class="merge-arrow"></div>
<div class="fact-box duplicate">
<div class="fact-text">${escapeHtml(d.fact2.text)}</div>
<div class="fact-meta">
ID: ${d.fact2.id}${d.fact2.confidence_score ? ' | Pewność: ' + (d.fact2.confidence_score * 100).toFixed(0) + '%' : ''}
${d.fact2.is_verified ? ' | ✅ Zweryfikowany' : ''}
</div>
</div>
</div>
<div class="duplicate-actions">
<button class="btn btn-secondary" onclick="skipDuplicate(${idx})">⏭️ Pomiń</button>
<button class="btn btn-primary" onclick="mergeFacts(${d.fact1.id}, ${d.fact2.id}, ${idx})">🔀 Połącz</button>
</div>
</div>
`).join('');
document.getElementById('duplicatesList').innerHTML = html;
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text || '';
return div.innerHTML;
}
async function mergeFacts(primaryId, duplicateId, idx) {
try {
const response = await fetch('/api/zopk/knowledge/fact-duplicates/merge', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-CSRFToken': '{{ csrf_token() }}'},
body: JSON.stringify({primary_id: primaryId, duplicate_id: duplicateId})
});
const data = await response.json();
if (data.success) {
document.getElementById('dup-' + idx).remove();
duplicatesData.splice(idx, 1);
document.getElementById('totalPairs').textContent = duplicatesData.length;
} else {
alert('Błąd: ' + data.error);
}
} catch (error) {
alert('Błąd: ' + error);
}
}
function skipDuplicate(idx) {
document.getElementById('dup-' + idx).remove();
duplicatesData.splice(idx, 1);
document.getElementById('totalPairs').textContent = duplicatesData.length;
}
async function mergeAllHigh() {
const highSim = duplicatesData.filter(d => d.similarity >= 0.9);
if (highSim.length === 0) {
alert('Brak duplikatów z podobieństwem >= 90%');
return;
}
if (!confirm(`Połączyć ${highSim.length} par z podobieństwem >= 90%?`)) return;
let merged = 0;
for (const d of highSim) {
try {
const response = await fetch('/api/zopk/knowledge/fact-duplicates/merge', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-CSRFToken': '{{ csrf_token() }}'},
body: JSON.stringify({primary_id: d.fact1.id, duplicate_id: d.fact2.id})
});
const data = await response.json();
if (data.success) merged++;
} catch (e) {}
}
alert(`Połączono ${merged}/${highSim.length} par`);
loadDuplicates();
}
{% endblock %}