- Add ai_relevant, ai_evaluation_reason, ai_evaluated_at columns to zopk_news - Add evaluate_news_relevance() and evaluate_pending_news() functions - Add /admin/zopk/news/evaluate-ai endpoint - Add AI filter tiles (Pasuje wg AI, Nie pasuje wg AI) - Add "Oceń przez AI" button with progress feedback - Show AI evaluation badge on news items - Add new sources: Norda FM, Twoja Telewizja Morska, Nadmorski24.pl, Facebook (Samsonowicz) AI evaluates news against ZOPK topics: offshore wind, nuclear plant, Kongsberg investment, data centers, hydrogen labs, key people. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1399 lines
43 KiB
HTML
1399 lines
43 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}ZOPK - Panel Admina - Norda Biznes Hub{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
.admin-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: var(--spacing-xl);
|
|
}
|
|
|
|
.admin-header h1 {
|
|
font-size: var(--font-size-2xl);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.admin-actions {
|
|
display: flex;
|
|
gap: var(--spacing-sm);
|
|
}
|
|
|
|
.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
gap: var(--spacing-lg);
|
|
margin-bottom: var(--spacing-xl);
|
|
}
|
|
|
|
.stat-card {
|
|
background: var(--surface);
|
|
padding: var(--spacing-lg);
|
|
border-radius: var(--radius-lg);
|
|
box-shadow: var(--shadow);
|
|
cursor: pointer;
|
|
transition: var(--transition);
|
|
border: 2px solid transparent;
|
|
text-decoration: none;
|
|
display: block;
|
|
color: inherit;
|
|
}
|
|
|
|
.stat-card:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: var(--shadow-lg);
|
|
}
|
|
|
|
.stat-card.info-only {
|
|
cursor: default;
|
|
opacity: 0.85;
|
|
}
|
|
|
|
.stat-card.info-only:hover {
|
|
transform: none;
|
|
box-shadow: var(--shadow);
|
|
}
|
|
|
|
.stat-card.active {
|
|
border-color: var(--primary);
|
|
background: var(--primary-light, #f0fdf4);
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: var(--font-size-3xl);
|
|
font-weight: 700;
|
|
color: var(--primary);
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.stat-card.warning .stat-value {
|
|
color: var(--warning);
|
|
}
|
|
|
|
.stat-card.success .stat-value {
|
|
color: var(--success);
|
|
}
|
|
|
|
.stat-card.danger .stat-value {
|
|
color: var(--danger);
|
|
}
|
|
|
|
/* Stats sections (two-row layout) */
|
|
.stats-section {
|
|
margin-bottom: var(--spacing-lg);
|
|
}
|
|
|
|
.stats-section-title {
|
|
font-size: var(--font-size-sm);
|
|
font-weight: 600;
|
|
color: var(--text-secondary);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
margin-bottom: var(--spacing-md);
|
|
}
|
|
|
|
.stats-section-title small {
|
|
font-weight: 400;
|
|
text-transform: none;
|
|
letter-spacing: normal;
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.stats-grid-small {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: var(--spacing-md);
|
|
max-width: 600px;
|
|
}
|
|
|
|
.filter-card {
|
|
cursor: pointer;
|
|
}
|
|
|
|
.filter-card:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: var(--shadow-lg);
|
|
}
|
|
|
|
/* AI Action button */
|
|
.stat-card.ai-action {
|
|
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
|
|
color: white;
|
|
border: none;
|
|
font-family: inherit;
|
|
}
|
|
|
|
.stat-card.ai-action .stat-value {
|
|
color: white;
|
|
}
|
|
|
|
.stat-card.ai-action .stat-label {
|
|
color: rgba(255,255,255,0.9);
|
|
}
|
|
|
|
.stat-card.ai-action:hover {
|
|
background: linear-gradient(135deg, #7c3aed 0%, #6d28d9 100%);
|
|
}
|
|
|
|
.stat-card.ai-action:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
transform: none;
|
|
}
|
|
|
|
.stat-card.ai-action:disabled:hover {
|
|
box-shadow: var(--shadow);
|
|
}
|
|
|
|
/* AI evaluation result */
|
|
.ai-result {
|
|
background: #f3e8ff;
|
|
border: 1px solid #c4b5fd;
|
|
color: #6b21a8;
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
border-radius: var(--radius);
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
.ai-result.success {
|
|
background: #dcfce7;
|
|
border-color: #86efac;
|
|
color: #166534;
|
|
}
|
|
|
|
.ai-result.error {
|
|
background: #fee2e2;
|
|
border-color: #fca5a5;
|
|
color: #991b1b;
|
|
}
|
|
|
|
/* AI badge in news list */
|
|
.ai-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
padding: 2px 8px;
|
|
border-radius: var(--radius-sm);
|
|
font-size: var(--font-size-xs);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.ai-badge.relevant {
|
|
background: #dcfce7;
|
|
color: #166534;
|
|
}
|
|
|
|
.ai-badge.not-relevant {
|
|
background: #fee2e2;
|
|
color: #991b1b;
|
|
}
|
|
|
|
/* Filters bar */
|
|
.filters-bar {
|
|
display: flex;
|
|
gap: var(--spacing-md);
|
|
margin-bottom: var(--spacing-lg);
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
}
|
|
|
|
.filter-group {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-xs);
|
|
}
|
|
|
|
.filter-group label {
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.filter-checkbox {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-xs);
|
|
padding: var(--spacing-xs) var(--spacing-sm);
|
|
background: var(--background);
|
|
border-radius: var(--radius);
|
|
cursor: pointer;
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
.filter-checkbox input {
|
|
margin: 0;
|
|
}
|
|
|
|
.old-news-warning {
|
|
background: #fef3c7;
|
|
color: #92400e;
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
border-radius: var(--radius);
|
|
font-size: var(--font-size-sm);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-sm);
|
|
}
|
|
|
|
/* Pagination */
|
|
.pagination {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
gap: var(--spacing-sm);
|
|
margin-top: var(--spacing-lg);
|
|
padding-top: var(--spacing-lg);
|
|
border-top: 1px solid var(--border);
|
|
}
|
|
|
|
.pagination a,
|
|
.pagination span {
|
|
padding: var(--spacing-xs) var(--spacing-sm);
|
|
border-radius: var(--radius);
|
|
font-size: var(--font-size-sm);
|
|
text-decoration: none;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.pagination a {
|
|
background: var(--background);
|
|
border: 1px solid var(--border);
|
|
}
|
|
|
|
.pagination a:hover {
|
|
background: var(--border);
|
|
}
|
|
|
|
.pagination .current {
|
|
background: var(--primary);
|
|
color: white;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.pagination .disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.pagination-info {
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
/* Old news badge */
|
|
.old-news-badge {
|
|
background: #fef3c7;
|
|
color: #92400e;
|
|
padding: 2px 6px;
|
|
border-radius: var(--radius-sm);
|
|
font-size: var(--font-size-xs);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.panel-section {
|
|
background: var(--surface);
|
|
border-radius: var(--radius-lg);
|
|
padding: var(--spacing-lg);
|
|
box-shadow: var(--shadow);
|
|
margin-bottom: var(--spacing-xl);
|
|
}
|
|
|
|
.panel-section h2 {
|
|
font-size: var(--font-size-lg);
|
|
margin-bottom: var(--spacing-lg);
|
|
padding-bottom: var(--spacing-md);
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.pending-news-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--spacing-md);
|
|
}
|
|
|
|
.pending-news-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
padding: var(--spacing-md);
|
|
background: var(--background);
|
|
border-radius: var(--radius);
|
|
gap: var(--spacing-md);
|
|
}
|
|
|
|
.news-info {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.news-info h4 {
|
|
font-size: var(--font-size-base);
|
|
margin-bottom: var(--spacing-xs);
|
|
word-break: break-word;
|
|
}
|
|
|
|
.news-info h4 a {
|
|
color: inherit;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.news-info h4 a:hover {
|
|
color: var(--primary);
|
|
}
|
|
|
|
.news-meta {
|
|
font-size: var(--font-size-xs);
|
|
color: var(--text-secondary);
|
|
display: flex;
|
|
gap: var(--spacing-md);
|
|
}
|
|
|
|
.news-actions {
|
|
display: flex;
|
|
gap: var(--spacing-xs);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.btn-approve {
|
|
background: var(--success);
|
|
color: white;
|
|
}
|
|
|
|
.btn-approve:hover {
|
|
background: #059669;
|
|
}
|
|
|
|
.btn-reject {
|
|
background: var(--danger);
|
|
color: white;
|
|
}
|
|
|
|
.btn-reject:hover {
|
|
background: #dc2626;
|
|
}
|
|
|
|
.projects-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
}
|
|
|
|
.projects-table th,
|
|
.projects-table td {
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
text-align: left;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.projects-table th {
|
|
font-weight: 600;
|
|
color: var(--text-secondary);
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
.status-badge {
|
|
display: inline-block;
|
|
padding: 2px 8px;
|
|
border-radius: var(--radius-sm);
|
|
font-size: var(--font-size-xs);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.status-planned { background: #fef3c7; color: #92400e; }
|
|
.status-in_progress { background: #dbeafe; color: #1e40af; }
|
|
.status-completed { background: #dcfce7; color: #166534; }
|
|
|
|
.search-section {
|
|
background: linear-gradient(135deg, #059669 0%, #047857 100%);
|
|
color: white;
|
|
padding: var(--spacing-lg);
|
|
border-radius: var(--radius-lg);
|
|
margin-bottom: var(--spacing-xl);
|
|
}
|
|
|
|
.search-section h3 {
|
|
margin-bottom: var(--spacing-md);
|
|
}
|
|
|
|
.search-form {
|
|
display: flex;
|
|
gap: var(--spacing-sm);
|
|
}
|
|
|
|
.search-form input {
|
|
flex: 1;
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
border: none;
|
|
border-radius: var(--radius);
|
|
font-size: var(--font-size-base);
|
|
}
|
|
|
|
.search-form button {
|
|
padding: var(--spacing-sm) var(--spacing-lg);
|
|
background: white;
|
|
color: var(--primary);
|
|
border: none;
|
|
border-radius: var(--radius);
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.search-form button:hover {
|
|
background: #f0fdf4;
|
|
}
|
|
|
|
.search-form button:disabled {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.fetch-jobs-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--spacing-sm);
|
|
}
|
|
|
|
.fetch-job {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
background: var(--background);
|
|
border-radius: var(--radius-sm);
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
.fetch-job-status {
|
|
padding: 2px 6px;
|
|
border-radius: var(--radius-sm);
|
|
font-size: var(--font-size-xs);
|
|
}
|
|
|
|
.fetch-job-status.completed { background: #dcfce7; color: #166534; }
|
|
.fetch-job-status.failed { background: #fee2e2; color: #991b1b; }
|
|
.fetch-job-status.running { background: #dbeafe; color: #1e40af; }
|
|
|
|
/* Progress bar */
|
|
.progress-container {
|
|
display: none;
|
|
margin-top: var(--spacing-lg);
|
|
background: rgba(255,255,255,0.1);
|
|
border-radius: var(--radius);
|
|
padding: var(--spacing-md);
|
|
}
|
|
|
|
.progress-container.active {
|
|
display: block;
|
|
}
|
|
|
|
.progress-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
margin-bottom: var(--spacing-sm);
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
.progress-bar-container {
|
|
height: 8px;
|
|
background: rgba(255,255,255,0.2);
|
|
border-radius: 4px;
|
|
overflow: hidden;
|
|
margin-bottom: var(--spacing-md);
|
|
}
|
|
|
|
.progress-bar-fill {
|
|
height: 100%;
|
|
background: white;
|
|
border-radius: 4px;
|
|
transition: width 0.3s ease;
|
|
width: 0%;
|
|
}
|
|
|
|
.progress-steps {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--spacing-xs);
|
|
font-size: var(--font-size-xs);
|
|
max-height: 200px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.progress-step {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-sm);
|
|
opacity: 0.6;
|
|
}
|
|
|
|
.progress-step.active {
|
|
opacity: 1;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.progress-step.completed {
|
|
opacity: 1;
|
|
}
|
|
|
|
.progress-step-icon {
|
|
width: 16px;
|
|
height: 16px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.progress-step.active .progress-step-icon::before {
|
|
content: "⏳";
|
|
}
|
|
|
|
.progress-step.completed .progress-step-icon::before {
|
|
content: "✓";
|
|
}
|
|
|
|
.progress-step.pending .progress-step-icon::before {
|
|
content: "○";
|
|
}
|
|
|
|
.progress-step-count {
|
|
margin-left: auto;
|
|
background: rgba(255,255,255,0.2);
|
|
padding: 2px 6px;
|
|
border-radius: var(--radius-sm);
|
|
}
|
|
|
|
/* Source stats */
|
|
.source-stats {
|
|
display: none;
|
|
margin-top: var(--spacing-md);
|
|
padding: var(--spacing-md);
|
|
background: rgba(255,255,255,0.1);
|
|
border-radius: var(--radius);
|
|
}
|
|
|
|
.source-stats.active {
|
|
display: block;
|
|
}
|
|
|
|
.source-stats h4 {
|
|
margin-bottom: var(--spacing-sm);
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
.source-stats-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
|
gap: var(--spacing-sm);
|
|
}
|
|
|
|
.source-stat-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
padding: var(--spacing-xs) var(--spacing-sm);
|
|
background: rgba(255,255,255,0.1);
|
|
border-radius: var(--radius-sm);
|
|
font-size: var(--font-size-xs);
|
|
}
|
|
|
|
.source-stat-item .count {
|
|
font-weight: 700;
|
|
}
|
|
|
|
/* News source badges */
|
|
.source-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
padding: 2px 8px;
|
|
border-radius: var(--radius-sm);
|
|
font-size: var(--font-size-xs);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.source-badge.brave { background: #fff3cd; color: #856404; }
|
|
.source-badge.rss_local_media { background: #d1ecf1; color: #0c5460; }
|
|
.source-badge.rss_government { background: #d4edda; color: #155724; }
|
|
.source-badge.rss_aggregator { background: #e2e3e5; color: #383d41; }
|
|
.source-badge.manual { background: #f8d7da; color: #721c24; }
|
|
|
|
.confidence-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
padding: 2px 8px;
|
|
border-radius: var(--radius-sm);
|
|
font-size: var(--font-size-xs);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.confidence-badge.high { background: #d4edda; color: #155724; }
|
|
.confidence-badge.medium { background: #fff3cd; color: #856404; }
|
|
.confidence-badge.low { background: #f8d7da; color: #721c24; }
|
|
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: var(--spacing-xl);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
/* 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;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.modal-overlay.active {
|
|
display: flex;
|
|
}
|
|
|
|
.modal {
|
|
background: var(--surface);
|
|
border-radius: var(--radius-lg);
|
|
padding: var(--spacing-xl);
|
|
max-width: 500px;
|
|
width: 90%;
|
|
max-height: 90vh;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.modal h3 {
|
|
margin-bottom: var(--spacing-lg);
|
|
}
|
|
|
|
.form-group {
|
|
margin-bottom: var(--spacing-md);
|
|
}
|
|
|
|
.form-group label {
|
|
display: block;
|
|
margin-bottom: var(--spacing-xs);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.form-group input,
|
|
.form-group textarea,
|
|
.form-group select {
|
|
width: 100%;
|
|
padding: var(--spacing-sm);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
font-size: var(--font-size-base);
|
|
}
|
|
|
|
.form-group textarea {
|
|
min-height: 80px;
|
|
resize: vertical;
|
|
}
|
|
|
|
.modal-actions {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: var(--spacing-sm);
|
|
margin-top: var(--spacing-lg);
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.admin-header {
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
gap: var(--spacing-md);
|
|
}
|
|
|
|
.pending-news-item {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.news-actions {
|
|
width: 100%;
|
|
justify-content: flex-end;
|
|
}
|
|
|
|
.search-form {
|
|
flex-direction: column;
|
|
}
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="admin-header">
|
|
<div>
|
|
<h1>Zielony Okręg Przemysłowy Kaszubia</h1>
|
|
<p class="text-muted">Panel zarządzania bazą wiedzy</p>
|
|
</div>
|
|
<div class="admin-actions">
|
|
<a href="{{ url_for('zopk_index') }}" class="btn btn-secondary" target="_blank">Zobacz stronę publiczną</a>
|
|
<a href="{{ url_for('admin_zopk_news') }}" class="btn btn-secondary">Zarządzaj newsami</a>
|
|
<button class="btn btn-primary" onclick="openAddNewsModal()">+ Dodaj news</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Section 1: General stats (info only, not clickable) -->
|
|
<div class="stats-section">
|
|
<h3 class="stats-section-title">Baza wiedzy ZOPK</h3>
|
|
<div class="stats-grid stats-grid-small">
|
|
<div class="stat-card info-only">
|
|
<div class="stat-value">{{ stats.total_projects }}</div>
|
|
<div class="stat-label">Projektów</div>
|
|
</div>
|
|
<div class="stat-card info-only">
|
|
<div class="stat-value">{{ stats.total_stakeholders }}</div>
|
|
<div class="stat-label">Interesariuszy</div>
|
|
</div>
|
|
<div class="stat-card info-only">
|
|
<div class="stat-value">{{ stats.total_resources }}</div>
|
|
<div class="stat-label">Materiałów</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Section 2: News filters (clickable) -->
|
|
<div class="stats-section">
|
|
<h3 class="stats-section-title">Filtruj newsy <small>(kliknij aby wybrać)</small></h3>
|
|
<div class="stats-grid stats-grid-small">
|
|
<a href="?status=pending" class="stat-card filter-card warning {{ 'active' if status_filter == 'pending' else '' }}">
|
|
<div class="stat-value">{{ stats.pending_news }}</div>
|
|
<div class="stat-label">Oczekujących</div>
|
|
</a>
|
|
<a href="?status=approved" class="stat-card filter-card success {{ 'active' if status_filter == 'approved' else '' }}">
|
|
<div class="stat-value">{{ stats.approved_news }}</div>
|
|
<div class="stat-label">Zatwierdzonych</div>
|
|
</a>
|
|
<a href="?status=rejected" class="stat-card filter-card danger {{ 'active' if status_filter == 'rejected' else '' }}">
|
|
<div class="stat-value">{{ stats.rejected_news }}</div>
|
|
<div class="stat-label">Odrzuconych</div>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Section 3: AI Evaluation filters -->
|
|
<div class="stats-section">
|
|
<h3 class="stats-section-title">Ocena AI (Gemini) <small>(kliknij aby wybrać)</small></h3>
|
|
<div class="stats-grid" style="grid-template-columns: repeat(4, 1fr); max-width: 800px;">
|
|
<a href="?status=ai_relevant" class="stat-card filter-card success {{ 'active' if status_filter == 'ai_relevant' else '' }}">
|
|
<div class="stat-value">{{ stats.ai_relevant }}</div>
|
|
<div class="stat-label">Pasuje wg AI</div>
|
|
</a>
|
|
<a href="?status=ai_not_relevant" class="stat-card filter-card danger {{ 'active' if status_filter == 'ai_not_relevant' else '' }}">
|
|
<div class="stat-value">{{ stats.ai_not_relevant }}</div>
|
|
<div class="stat-label">Nie pasuje wg AI</div>
|
|
</a>
|
|
<div class="stat-card info-only">
|
|
<div class="stat-value">{{ stats.ai_not_evaluated }}</div>
|
|
<div class="stat-label">Nieocenione</div>
|
|
</div>
|
|
<button type="button" class="stat-card filter-card ai-action" onclick="evaluateWithAI()" id="aiEvalBtn" {% if stats.ai_not_evaluated == 0 %}disabled{% endif %}>
|
|
<div class="stat-value" style="font-size: var(--font-size-xl);">🤖</div>
|
|
<div class="stat-label">Oceń przez AI</div>
|
|
</button>
|
|
</div>
|
|
<div id="aiEvalResult" style="margin-top: var(--spacing-md); display: none;"></div>
|
|
</div>
|
|
|
|
<!-- Old news warning -->
|
|
{% if stats.old_news > 0 and not show_old %}
|
|
<div class="old-news-warning">
|
|
<span>⚠️</span>
|
|
<span><strong>{{ stats.old_news }} newsów</strong> z przed 2024 roku zostało ukrytych (ZOPK powstał w 2024).
|
|
<a href="?status={{ status_filter }}&show_old=true">Pokaż wszystkie</a> lub
|
|
<a href="#" onclick="rejectOldNews(); return false;">odrzuć wszystkie stare</a>.</span>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Search Section -->
|
|
<div class="search-section">
|
|
<h3>Wyszukaj nowe artykuły</h3>
|
|
<p style="opacity: 0.9; margin-bottom: var(--spacing-md); font-size: var(--font-size-sm);">Multi-source search: Brave API, RSS (trojmiasto.pl, Dziennik Bałtycki), Google News, gov.pl</p>
|
|
<div class="search-form">
|
|
<input type="text" id="searchQuery" value="Zielony Okręg Przemysłowy Kaszubia" placeholder="Wpisz zapytanie...">
|
|
<button type="button" onclick="searchNews()" id="searchBtn">Szukaj artykułów</button>
|
|
</div>
|
|
|
|
<!-- Progress Container -->
|
|
<div class="progress-container" id="progressContainer">
|
|
<div class="progress-header">
|
|
<span id="progressStatus">Inicjalizacja...</span>
|
|
<span id="progressPercent">0%</span>
|
|
</div>
|
|
<div class="progress-bar-container">
|
|
<div class="progress-bar-fill" id="progressBar"></div>
|
|
</div>
|
|
<div class="progress-steps" id="progressSteps"></div>
|
|
</div>
|
|
|
|
<!-- Source Stats (shown after completion) -->
|
|
<div class="source-stats" id="sourceStats">
|
|
<h4>Statystyki źródeł</h4>
|
|
<div class="source-stats-grid" id="sourceStatsGrid"></div>
|
|
</div>
|
|
|
|
<div id="searchResult" style="margin-top: var(--spacing-md); display: none;"></div>
|
|
</div>
|
|
|
|
<!-- News List -->
|
|
<div class="panel-section">
|
|
<h2>
|
|
{% if status_filter == 'pending' %}Newsy oczekujące na moderację
|
|
{% elif status_filter == 'approved' %}Newsy zatwierdzone
|
|
{% elif status_filter == 'rejected' %}Newsy odrzucone
|
|
{% elif status_filter == 'ai_relevant' %}🤖 Newsy pasujące wg AI
|
|
{% elif status_filter == 'ai_not_relevant' %}🤖 Newsy NIE pasujące wg AI
|
|
{% else %}Wszystkie newsy{% endif %}
|
|
({{ total_news_filtered }})
|
|
</h2>
|
|
|
|
{% if news_items %}
|
|
<div class="pending-news-list">
|
|
{% for news in news_items %}
|
|
<div class="pending-news-item" id="news-{{ news.id }}">
|
|
<div class="news-info">
|
|
<h4><a href="{{ news.url }}" target="_blank" rel="noopener">{{ news.title }}</a></h4>
|
|
<div class="news-meta">
|
|
{% if news.source_type %}
|
|
<span class="source-badge {{ news.source_type|replace(' ', '_') }}">
|
|
{% if news.source_type == 'brave' %}🔍 Brave
|
|
{% elif news.source_type == 'rss_local_media' %}📰 Media lokalne
|
|
{% elif news.source_type == 'rss_government' %}🏛️ Rząd
|
|
{% elif news.source_type == 'rss_aggregator' %}📡 Agregator
|
|
{% elif news.source_type == 'manual' %}✏️ Ręcznie
|
|
{% else %}{{ news.source_type }}{% endif %}
|
|
</span>
|
|
{% endif %}
|
|
<span>{{ news.source_name or news.source_domain or '-' }}</span>
|
|
{% set news_year = news.published_at.year if news.published_at else None %}
|
|
<span>{{ news.published_at.strftime('%d.%m.%Y') if news.published_at else (news.created_at.strftime('%d.%m.%Y') if news.created_at else '-') }}</span>
|
|
{% if news_year and news_year < min_year %}
|
|
<span class="old-news-badge">⚠️ Sprzed {{ min_year }}</span>
|
|
{% endif %}
|
|
{% if news.confidence_score %}
|
|
<span class="confidence-badge {{ 'high' if news.confidence_score >= 4 else ('medium' if news.confidence_score >= 2 else 'low') }}">
|
|
{% if news.source_count and news.source_count > 1 %}{{ news.source_count }} źródeł{% else %}1 źródło{% endif %}
|
|
</span>
|
|
{% endif %}
|
|
{% if news.status == 'approved' or news.status == 'auto_approved' %}
|
|
<span class="confidence-badge high">✓ {{ 'Auto' if news.status == 'auto_approved' else '' }}Zatwierdzony</span>
|
|
{% elif news.status == 'rejected' %}
|
|
<span class="confidence-badge low">✗ Odrzucony</span>
|
|
{% endif %}
|
|
{# AI Evaluation badge #}
|
|
{% if news.ai_relevant is not none %}
|
|
<span class="ai-badge {{ 'relevant' if news.ai_relevant else 'not-relevant' }}" title="{{ news.ai_evaluation_reason or '' }}">
|
|
🤖 {{ 'Pasuje' if news.ai_relevant else 'Nie pasuje' }}
|
|
</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<div class="news-actions">
|
|
{% if news.status == 'pending' %}
|
|
<button class="btn btn-sm btn-approve" onclick="approveNews({{ news.id }})">Zatwierdź</button>
|
|
<button class="btn btn-sm btn-reject" onclick="rejectNews({{ news.id }})">Odrzuć</button>
|
|
{% elif news.status == 'rejected' %}
|
|
<button class="btn btn-sm btn-approve" onclick="approveNews({{ news.id }})">Przywróć</button>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
{% if total_pages > 1 %}
|
|
<div class="pagination">
|
|
{% if current_page > 1 %}
|
|
<a href="?page={{ current_page - 1 }}&status={{ status_filter }}{% if show_old %}&show_old=true{% endif %}">← Poprzednia</a>
|
|
{% else %}
|
|
<span class="disabled">← Poprzednia</span>
|
|
{% endif %}
|
|
|
|
{% for p in range(1, total_pages + 1) %}
|
|
{% if p == current_page %}
|
|
<span class="current">{{ p }}</span>
|
|
{% elif p == 1 or p == total_pages or (p >= current_page - 2 and p <= current_page + 2) %}
|
|
<a href="?page={{ p }}&status={{ status_filter }}{% if show_old %}&show_old=true{% endif %}">{{ p }}</a>
|
|
{% elif p == current_page - 3 or p == current_page + 3 %}
|
|
<span>...</span>
|
|
{% endif %}
|
|
{% endfor %}
|
|
|
|
{% if current_page < total_pages %}
|
|
<a href="?page={{ current_page + 1 }}&status={{ status_filter }}{% if show_old %}&show_old=true{% endif %}">Następna →</a>
|
|
{% else %}
|
|
<span class="disabled">Następna →</span>
|
|
{% endif %}
|
|
|
|
<span class="pagination-info">
|
|
({{ (current_page - 1) * per_page + 1 }}-{{ [current_page * per_page, total_news_filtered]|min }} z {{ total_news_filtered }})
|
|
</span>
|
|
</div>
|
|
{% endif %}
|
|
{% else %}
|
|
<div class="empty-state">
|
|
<p>Brak newsów{% if status_filter != 'all' %} o tym statusie{% endif %}.</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Projects -->
|
|
<div class="panel-section">
|
|
<h2>Projekty strategiczne</h2>
|
|
|
|
{% if projects %}
|
|
<table class="projects-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Nazwa</th>
|
|
<th>Typ</th>
|
|
<th>Status</th>
|
|
<th>Region</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for project in projects %}
|
|
<tr>
|
|
<td>
|
|
<strong>{{ project.name }}</strong>
|
|
<br><small class="text-muted">{{ project.slug }}</small>
|
|
</td>
|
|
<td>{{ project.project_type or '-' }}</td>
|
|
<td>
|
|
<span class="status-badge status-{{ project.status }}">
|
|
{% if project.status == 'planned' %}Planowany{% elif project.status == 'in_progress' %}W realizacji{% else %}{{ project.status }}{% endif %}
|
|
</span>
|
|
</td>
|
|
<td>{{ project.region or '-' }}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
{% else %}
|
|
<div class="empty-state">
|
|
<p>Brak projektów.</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Recent Fetch Jobs -->
|
|
{% if fetch_jobs %}
|
|
<div class="panel-section">
|
|
<h2>Ostatnie wyszukiwania</h2>
|
|
<div class="fetch-jobs-list">
|
|
{% for job in fetch_jobs %}
|
|
<div class="fetch-job">
|
|
<div>
|
|
<strong>{{ job.search_query }}</strong>
|
|
<br><small class="text-muted">{{ job.created_at.strftime('%d.%m.%Y %H:%M') if job.created_at else '-' }}</small>
|
|
</div>
|
|
<div>
|
|
Znaleziono: {{ job.results_found or 0 }} | Nowych: {{ job.results_new or 0 }}
|
|
</div>
|
|
<span class="fetch-job-status {{ job.status }}">{{ job.status }}</span>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Add News Modal -->
|
|
<div class="modal-overlay" id="addNewsModal">
|
|
<div class="modal">
|
|
<h3>Dodaj news ręcznie</h3>
|
|
<form id="addNewsForm">
|
|
<div class="form-group">
|
|
<label for="newsTitle">Tytuł *</label>
|
|
<input type="text" id="newsTitle" name="title" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="newsUrl">URL artykułu *</label>
|
|
<input type="url" id="newsUrl" name="url" required placeholder="https://...">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="newsDescription">Opis</label>
|
|
<textarea id="newsDescription" name="description" placeholder="Krótki opis artykułu..."></textarea>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="newsSource">Źródło</label>
|
|
<input type="text" id="newsSource" name="source_name" placeholder="np. trojmiasto.pl">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="newsProject">Projekt</label>
|
|
<select id="newsProject" name="project_id">
|
|
<option value="">-- Brak --</option>
|
|
{% for project in projects %}
|
|
<option value="{{ project.id }}">{{ project.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="modal-actions">
|
|
<button type="button" class="btn btn-secondary" onclick="closeAddNewsModal()">Anuluj</button>
|
|
<button type="submit" class="btn btn-primary">Dodaj</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
const csrfToken = '{{ csrf_token() }}';
|
|
|
|
function openAddNewsModal() {
|
|
document.getElementById('addNewsModal').classList.add('active');
|
|
}
|
|
|
|
function closeAddNewsModal() {
|
|
document.getElementById('addNewsModal').classList.remove('active');
|
|
document.getElementById('addNewsForm').reset();
|
|
}
|
|
|
|
document.getElementById('addNewsForm').addEventListener('submit', async function(e) {
|
|
e.preventDefault();
|
|
|
|
const formData = {
|
|
title: document.getElementById('newsTitle').value,
|
|
url: document.getElementById('newsUrl').value,
|
|
description: document.getElementById('newsDescription').value,
|
|
source_name: document.getElementById('newsSource').value,
|
|
project_id: document.getElementById('newsProject').value || null
|
|
};
|
|
|
|
try {
|
|
const response = await fetch('{{ url_for("admin_zopk_news_add") }}', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': csrfToken
|
|
},
|
|
body: JSON.stringify(formData)
|
|
});
|
|
|
|
const data = await response.json();
|
|
if (data.success) {
|
|
alert('News został dodany.');
|
|
closeAddNewsModal();
|
|
location.reload();
|
|
} else {
|
|
alert(data.error || 'Wystąpił błąd');
|
|
}
|
|
} catch (error) {
|
|
alert('Błąd połączenia: ' + error.message);
|
|
}
|
|
});
|
|
|
|
async function approveNews(newsId) {
|
|
if (!confirm('Czy na pewno chcesz zatwierdzić ten news?')) return;
|
|
|
|
try {
|
|
const response = await fetch(`/admin/zopk/news/${newsId}/approve`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': csrfToken
|
|
}
|
|
});
|
|
|
|
const data = await response.json();
|
|
if (data.success) {
|
|
document.getElementById(`news-${newsId}`).remove();
|
|
} else {
|
|
alert(data.error || 'Wystąpił błąd');
|
|
}
|
|
} catch (error) {
|
|
alert('Błąd połączenia: ' + error.message);
|
|
}
|
|
}
|
|
|
|
async function rejectNews(newsId) {
|
|
const reason = prompt('Powód odrzucenia (opcjonalnie):');
|
|
if (reason === null) return; // User cancelled
|
|
|
|
try {
|
|
const response = await fetch(`/admin/zopk/news/${newsId}/reject`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': csrfToken
|
|
},
|
|
body: JSON.stringify({ reason: reason })
|
|
});
|
|
|
|
const data = await response.json();
|
|
if (data.success) {
|
|
document.getElementById(`news-${newsId}`).remove();
|
|
} else {
|
|
alert(data.error || 'Wystąpił błąd');
|
|
}
|
|
} catch (error) {
|
|
alert('Błąd połączenia: ' + error.message);
|
|
}
|
|
}
|
|
|
|
async function rejectOldNews() {
|
|
const minYear = {{ min_year }};
|
|
if (!confirm(`Czy na pewno chcesz odrzucić wszystkie newsy sprzed ${minYear} roku?\n\nZOPK powstał w 2024 roku, więc starsze artykuły najprawdopodobniej nie dotyczą tego projektu.`)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch('/admin/zopk/news/reject-old', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': csrfToken
|
|
},
|
|
body: JSON.stringify({ min_year: minYear })
|
|
});
|
|
|
|
const data = await response.json();
|
|
if (data.success) {
|
|
alert(data.message);
|
|
location.reload();
|
|
} else {
|
|
alert(data.error || 'Wystąpił błąd');
|
|
}
|
|
} catch (error) {
|
|
alert('Błąd połączenia: ' + error.message);
|
|
}
|
|
}
|
|
|
|
// AI Evaluation function
|
|
async function evaluateWithAI() {
|
|
const btn = document.getElementById('aiEvalBtn');
|
|
const resultDiv = document.getElementById('aiEvalResult');
|
|
|
|
if (!confirm('Czy chcesz uruchomić ocenę AI (Gemini) dla nieocenionych newsów?\n\nProces może potrwać kilka minut. Ocenionych zostanie max 50 newsów.')) {
|
|
return;
|
|
}
|
|
|
|
btn.disabled = true;
|
|
btn.querySelector('.stat-label').textContent = 'Oceniam...';
|
|
resultDiv.style.display = 'block';
|
|
resultDiv.className = 'ai-result';
|
|
resultDiv.innerHTML = '🤖 Trwa ocena newsów przez AI... Proszę czekać.';
|
|
|
|
try {
|
|
const response = await fetch('/admin/zopk/news/evaluate-ai', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': csrfToken
|
|
},
|
|
body: JSON.stringify({ limit: 50 })
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
resultDiv.className = 'ai-result success';
|
|
resultDiv.innerHTML = `
|
|
✓ ${data.message}<br>
|
|
<small>Pasuje: ${data.relevant_count} | Nie pasuje: ${data.not_relevant_count} | Błędy: ${data.errors}</small>
|
|
`;
|
|
|
|
// Refresh page after 3 seconds
|
|
setTimeout(() => {
|
|
location.reload();
|
|
}, 3000);
|
|
} else {
|
|
resultDiv.className = 'ai-result error';
|
|
resultDiv.innerHTML = `✗ Błąd: ${data.error}`;
|
|
btn.disabled = false;
|
|
btn.querySelector('.stat-label').textContent = 'Oceń przez AI';
|
|
}
|
|
} catch (error) {
|
|
resultDiv.className = 'ai-result error';
|
|
resultDiv.innerHTML = `✗ Błąd połączenia: ${error.message}`;
|
|
btn.disabled = false;
|
|
btn.querySelector('.stat-label').textContent = 'Oceń przez AI';
|
|
}
|
|
}
|
|
|
|
// Source names mapping for progress display
|
|
const SOURCE_NAMES = {
|
|
'brave': '🔍 Brave Search API',
|
|
'trojmiasto': '📰 trojmiasto.pl',
|
|
'dziennik_baltycki': '📰 Dziennik Bałtycki',
|
|
'gov_mon': '🏛️ Ministerstwo Obrony Narodowej',
|
|
'gov_przemysl': '🏛️ Min. Rozwoju i Technologii',
|
|
'google_news_zopk': '📡 Google News (ZOPK)',
|
|
'google_news_offshore': '📡 Google News (offshore)',
|
|
'google_news_nuclear': '📡 Google News (elektrownia jądrowa)',
|
|
'google_news_samsonowicz': '📡 Google News (Samsonowicz)',
|
|
'google_news_kongsberg': '📡 Google News (Kongsberg)',
|
|
// New local media sources
|
|
'google_news_norda_fm': '📻 Norda FM',
|
|
'google_news_ttm': '📺 Twoja Telewizja Morska',
|
|
'google_news_nadmorski24': '📰 Nadmorski24.pl',
|
|
'google_news_samsonowicz_fb': '👤 Facebook (Samsonowicz)',
|
|
'google_news_norda': '📡 Google News (Norda Biznes)',
|
|
'google_news_spoko': '📡 Google News (Spoko Gospodarcze)'
|
|
};
|
|
|
|
const ALL_SOURCES = Object.keys(SOURCE_NAMES);
|
|
|
|
async function searchNews() {
|
|
const btn = document.getElementById('searchBtn');
|
|
const resultDiv = document.getElementById('searchResult');
|
|
const progressContainer = document.getElementById('progressContainer');
|
|
const progressBar = document.getElementById('progressBar');
|
|
const progressStatus = document.getElementById('progressStatus');
|
|
const progressPercent = document.getElementById('progressPercent');
|
|
const progressSteps = document.getElementById('progressSteps');
|
|
const sourceStats = document.getElementById('sourceStats');
|
|
const sourceStatsGrid = document.getElementById('sourceStatsGrid');
|
|
const query = document.getElementById('searchQuery').value;
|
|
|
|
// Reset UI
|
|
btn.disabled = true;
|
|
btn.textContent = 'Szukam...';
|
|
resultDiv.style.display = 'none';
|
|
sourceStats.classList.remove('active');
|
|
progressContainer.classList.add('active');
|
|
progressBar.style.width = '0%';
|
|
progressPercent.textContent = '0%';
|
|
|
|
// Build initial progress steps
|
|
progressSteps.innerHTML = ALL_SOURCES.map((src, idx) => `
|
|
<div class="progress-step pending" id="step-${src}">
|
|
<span class="progress-step-icon"></span>
|
|
<span>${SOURCE_NAMES[src]}</span>
|
|
<span class="progress-step-count" id="count-${src}">-</span>
|
|
</div>
|
|
`).join('');
|
|
|
|
// Simulate progress while waiting for API
|
|
let currentStep = 0;
|
|
const totalSteps = ALL_SOURCES.length + 1; // +1 for cross-verification
|
|
|
|
const progressInterval = setInterval(() => {
|
|
if (currentStep < ALL_SOURCES.length) {
|
|
// Mark previous step as completed
|
|
if (currentStep > 0) {
|
|
const prevStep = document.getElementById(`step-${ALL_SOURCES[currentStep - 1]}`);
|
|
if (prevStep) {
|
|
prevStep.classList.remove('active');
|
|
prevStep.classList.add('completed');
|
|
}
|
|
}
|
|
|
|
// Mark current step as active
|
|
const currStep = document.getElementById(`step-${ALL_SOURCES[currentStep]}`);
|
|
if (currStep) {
|
|
currStep.classList.remove('pending');
|
|
currStep.classList.add('active');
|
|
}
|
|
|
|
progressStatus.textContent = `Przeszukiwanie: ${SOURCE_NAMES[ALL_SOURCES[currentStep]]}`;
|
|
const percent = Math.round(((currentStep + 1) / totalSteps) * 80);
|
|
progressBar.style.width = `${percent}%`;
|
|
progressPercent.textContent = `${percent}%`;
|
|
|
|
currentStep++;
|
|
}
|
|
}, 800);
|
|
|
|
try {
|
|
const response = await fetch('{{ url_for("api_zopk_search_news") }}', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': csrfToken
|
|
},
|
|
body: JSON.stringify({ query: query })
|
|
});
|
|
|
|
clearInterval(progressInterval);
|
|
|
|
const data = await response.json();
|
|
|
|
// Mark all steps as completed
|
|
ALL_SOURCES.forEach(src => {
|
|
const step = document.getElementById(`step-${src}`);
|
|
if (step) {
|
|
step.classList.remove('pending', 'active');
|
|
step.classList.add('completed');
|
|
}
|
|
});
|
|
|
|
if (data.success) {
|
|
// Update counts from source_stats
|
|
if (data.source_stats) {
|
|
Object.entries(data.source_stats).forEach(([src, count]) => {
|
|
const countEl = document.getElementById(`count-${src}`);
|
|
if (countEl) {
|
|
countEl.textContent = count;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Show cross-verification step
|
|
progressStatus.textContent = 'Weryfikacja krzyżowa zakończona';
|
|
progressBar.style.width = '100%';
|
|
progressPercent.textContent = '100%';
|
|
|
|
// Show source stats
|
|
if (data.source_stats && Object.keys(data.source_stats).length > 0) {
|
|
sourceStatsGrid.innerHTML = Object.entries(data.source_stats)
|
|
.filter(([src, count]) => count > 0)
|
|
.sort((a, b) => b[1] - a[1])
|
|
.map(([src, count]) => `
|
|
<div class="source-stat-item">
|
|
<span>${SOURCE_NAMES[src] || src}</span>
|
|
<span class="count">${count}</span>
|
|
</div>
|
|
`).join('');
|
|
sourceStats.classList.add('active');
|
|
}
|
|
|
|
// Show result message
|
|
resultDiv.style.display = 'block';
|
|
resultDiv.innerHTML = `
|
|
<p style="color: #dcfce7;">
|
|
✓ ${data.message}<br>
|
|
<small>Auto-zatwierdzone (3+ źródeł): ${data.auto_approved || 0}</small>
|
|
</p>
|
|
`;
|
|
|
|
// Auto-refresh after 3 seconds
|
|
setTimeout(() => {
|
|
progressStatus.textContent = 'Odświeżanie strony...';
|
|
location.reload();
|
|
}, 3000);
|
|
} else {
|
|
progressBar.style.width = '100%';
|
|
progressBar.style.background = '#fca5a5';
|
|
progressStatus.textContent = 'Błąd wyszukiwania';
|
|
resultDiv.style.display = 'block';
|
|
resultDiv.innerHTML = `<p style="color: #fca5a5;">Błąd: ${data.error}</p>`;
|
|
btn.disabled = false;
|
|
btn.textContent = 'Szukaj artykułów';
|
|
}
|
|
} catch (error) {
|
|
clearInterval(progressInterval);
|
|
progressBar.style.width = '100%';
|
|
progressBar.style.background = '#fca5a5';
|
|
progressStatus.textContent = 'Błąd połączenia';
|
|
resultDiv.style.display = 'block';
|
|
resultDiv.innerHTML = `<p style="color: #fca5a5;">Błąd połączenia: ${error.message}</p>`;
|
|
btn.disabled = false;
|
|
btn.textContent = 'Szukaj artykułów';
|
|
}
|
|
}
|
|
|
|
// Close modal on click outside
|
|
document.getElementById('addNewsModal').addEventListener('click', function(e) {
|
|
if (e.target === this) {
|
|
closeAddNewsModal();
|
|
}
|
|
});
|
|
{% endblock %}
|