- Zmiana nazwy: "Norda Biznes Hub" → "Norda Biznes Partner" - Aktualizacja modelu AI: Gemini 2.0 Flash → Gemini 3 Flash - Zachowano historyczne odniesienia w timeline i dokumentacji Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
3073 lines
107 KiB
HTML
3073 lines
107 KiB
HTML
{% extends "base.html" %}
|
||
|
||
{% block title %}ZOP Kaszubia - Panel Admina - Norda Biznes Partner{% 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;
|
||
}
|
||
|
||
/* Star rating display */
|
||
.ai-stars {
|
||
display: inline-flex;
|
||
gap: 1px;
|
||
font-size: 11px;
|
||
letter-spacing: -1px;
|
||
}
|
||
|
||
.ai-stars .star-filled { color: #f59e0b; }
|
||
.ai-stars .star-empty { color: #d1d5db; }
|
||
|
||
.ai-badge.score-5 { background: #dcfce7; color: #166534; }
|
||
.ai-badge.score-4 { background: #d1fae5; color: #047857; }
|
||
.ai-badge.score-3 { background: #fef3c7; color: #92400e; }
|
||
.ai-badge.score-2 { background: #fee2e2; color: #b91c1c; }
|
||
.ai-badge.score-1 { background: #fecaca; color: #991b1b; }
|
||
|
||
/* AI Evaluation Modal */
|
||
.modal-icon {
|
||
font-size: 48px;
|
||
text-align: center;
|
||
margin-bottom: var(--spacing-md);
|
||
}
|
||
|
||
.modal-icon.spinning {
|
||
animation: spin 2s linear infinite;
|
||
}
|
||
|
||
@keyframes spin {
|
||
from { transform: rotate(0deg); }
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
|
||
.modal-description {
|
||
text-align: center;
|
||
color: var(--text-primary);
|
||
margin-bottom: var(--spacing-sm);
|
||
}
|
||
|
||
.modal-note {
|
||
text-align: center;
|
||
font-size: var(--font-size-sm);
|
||
color: var(--text-secondary);
|
||
margin-bottom: var(--spacing-lg);
|
||
}
|
||
|
||
.ai-progress-bar {
|
||
width: 100%;
|
||
height: 8px;
|
||
background: var(--border);
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
margin-bottom: var(--spacing-md);
|
||
}
|
||
|
||
.ai-progress-fill {
|
||
height: 100%;
|
||
background: linear-gradient(90deg, #8b5cf6, #7c3aed);
|
||
border-radius: 4px;
|
||
transition: width 0.5s ease;
|
||
width: 0%;
|
||
}
|
||
|
||
.ai-result-stats {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: var(--spacing-md);
|
||
margin-bottom: var(--spacing-lg);
|
||
}
|
||
|
||
.ai-stat-item {
|
||
text-align: center;
|
||
padding: var(--spacing-md);
|
||
background: var(--background);
|
||
border-radius: var(--radius);
|
||
}
|
||
|
||
.ai-stat-item .value {
|
||
font-size: var(--font-size-2xl);
|
||
font-weight: 700;
|
||
}
|
||
|
||
.ai-stat-item .label {
|
||
font-size: var(--font-size-sm);
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.ai-stat-item.success .value { color: var(--success); }
|
||
.ai-stat-item.danger .value { color: var(--danger); }
|
||
.ai-stat-item.warning .value { color: var(--warning); }
|
||
|
||
#aiResultIcon.success { color: var(--success); }
|
||
#aiResultIcon.error { color: var(--danger); }
|
||
|
||
/* 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;
|
||
}
|
||
}
|
||
|
||
/* Progress phases (search → filter → AI → save) */
|
||
.progress-phases {
|
||
display: flex;
|
||
gap: var(--spacing-xs);
|
||
margin-bottom: var(--spacing-md);
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.progress-phase {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 6px 12px;
|
||
border-radius: var(--radius);
|
||
font-size: var(--font-size-xs);
|
||
background: rgba(255,255,255,0.1);
|
||
opacity: 0.5;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.progress-phase.active {
|
||
opacity: 1;
|
||
background: rgba(255,255,255,0.25);
|
||
animation: pulse 1.5s ease-in-out infinite;
|
||
}
|
||
|
||
.progress-phase.completed {
|
||
opacity: 1;
|
||
background: rgba(34, 197, 94, 0.3);
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0%, 100% { transform: scale(1); }
|
||
50% { transform: scale(1.02); }
|
||
}
|
||
|
||
.progress-phase-icon {
|
||
font-size: 1em;
|
||
}
|
||
|
||
/* Search results container */
|
||
.search-results-container {
|
||
margin-top: var(--spacing-lg);
|
||
padding: var(--spacing-lg);
|
||
background: rgba(255,255,255,0.1);
|
||
border-radius: var(--radius-lg);
|
||
animation: fadeIn 0.5s ease;
|
||
}
|
||
|
||
@keyframes fadeIn {
|
||
from { opacity: 0; transform: translateY(-10px); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
|
||
.search-results-summary {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||
gap: var(--spacing-md);
|
||
margin-bottom: var(--spacing-lg);
|
||
}
|
||
|
||
.summary-stat {
|
||
text-align: center;
|
||
padding: var(--spacing-md);
|
||
background: rgba(255,255,255,0.1);
|
||
border-radius: var(--radius);
|
||
}
|
||
|
||
.summary-stat .value {
|
||
font-size: var(--font-size-2xl);
|
||
font-weight: 700;
|
||
}
|
||
|
||
.summary-stat .label {
|
||
font-size: var(--font-size-xs);
|
||
opacity: 0.8;
|
||
}
|
||
|
||
.summary-stat.success .value { color: #86efac; }
|
||
.summary-stat.warning .value { color: #fde68a; }
|
||
.summary-stat.error .value { color: #fca5a5; }
|
||
.summary-stat.info .value { color: #93c5fd; }
|
||
|
||
/* Auto-approved articles section */
|
||
.auto-approved-section {
|
||
margin-top: var(--spacing-lg);
|
||
padding: var(--spacing-md);
|
||
background: rgba(34, 197, 94, 0.15);
|
||
border-radius: var(--radius);
|
||
border: 1px solid rgba(34, 197, 94, 0.3);
|
||
}
|
||
|
||
.auto-approved-section h4 {
|
||
margin-bottom: var(--spacing-md);
|
||
font-size: var(--font-size-sm);
|
||
}
|
||
|
||
.auto-approved-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--spacing-xs);
|
||
max-height: 200px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.auto-approved-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--spacing-sm);
|
||
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);
|
||
}
|
||
|
||
.auto-approved-item .stars {
|
||
color: #fbbf24;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.auto-approved-item .title {
|
||
flex: 1;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.auto-approved-item .source {
|
||
color: rgba(255,255,255,0.6);
|
||
flex-shrink: 0;
|
||
font-size: 10px;
|
||
}
|
||
|
||
/* Refresh countdown */
|
||
.refresh-countdown {
|
||
margin-top: var(--spacing-lg);
|
||
padding: var(--spacing-md);
|
||
background: rgba(255,255,255,0.1);
|
||
border-radius: var(--radius);
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
font-size: var(--font-size-sm);
|
||
}
|
||
|
||
.refresh-countdown strong {
|
||
font-size: var(--font-size-lg);
|
||
color: #fde68a;
|
||
}
|
||
</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 ZOP Kaszubia</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>
|
||
|
||
<!-- AI Model Info Banner -->
|
||
<div class="ai-model-info" style="background: linear-gradient(135deg, #e0f2fe 0%, #bae6fd 100%); border: 1px solid #7dd3fc; border-radius: var(--radius); padding: var(--spacing-sm) var(--spacing-md); margin-bottom: var(--spacing-md); font-size: var(--font-size-sm);">
|
||
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: var(--spacing-xs);">
|
||
<div>
|
||
<strong>🤖 Model:</strong> <code style="background: #fff; padding: 2px 6px; border-radius: 4px;">gemini-2.5-flash-lite</code>
|
||
</div>
|
||
<div>
|
||
<strong>📅 Prompt v2:</strong> 2026-01-15
|
||
</div>
|
||
<div title="Via Pomerania, S6, Droga Czerwona, Pakt Bezpieczeństwa, NORDA, Deklaracja Bałtycka">
|
||
<strong>✅ Nowe tematy:</strong> +7 projektów infrastrukturalnych
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<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>
|
||
<a href="?status=ai_not_evaluated" class="stat-card filter-card warning {{ 'active' if status_filter == 'ai_not_evaluated' else '' }}">
|
||
<div class="stat-value">{{ stats.ai_not_evaluated }}</div>
|
||
<div class="stat-label">Nieocenione</div>
|
||
</a>
|
||
<button type="button" class="stat-card filter-card ai-action" onclick="evaluateWithAI()" id="aiEvalBtn" {% if stats.ai_not_evaluated == 0 %}disabled{% endif %} title="Gemini AI oceni {{ stats.ai_not_evaluated }} nieocenionych newsów">
|
||
<div class="stat-value" style="font-size: var(--font-size-xl);">🤖</div>
|
||
<div class="stat-label">AI: Oceń {{ stats.ai_not_evaluated }}</div>
|
||
</button>
|
||
</div>
|
||
|
||
{% if stats.ai_missing_score > 0 %}
|
||
<div class="stats-grid" style="grid-template-columns: repeat(2, 1fr); max-width: 400px; margin-top: var(--spacing-md);">
|
||
<div class="stat-card warning info-only">
|
||
<div class="stat-value">{{ stats.ai_missing_score }}</div>
|
||
<div class="stat-label">Brak gwiazdek</div>
|
||
</div>
|
||
<button type="button" class="stat-card filter-card ai-action" onclick="reevaluateScores()" id="aiRescoreBtn" title="Gemini AI przeoceni {{ stats.ai_missing_score }} newsów i doda oceny 1-5 gwiazdek">
|
||
<div class="stat-value" style="font-size: var(--font-size-xl);">🤖⭐</div>
|
||
<div class="stat-label">AI: Dodaj gwiazdki</div>
|
||
</button>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- Re-evaluate low score news with key topics (Via Pomerania, NORDA, etc.) -->
|
||
<div class="stats-grid" style="grid-template-columns: repeat(1, 1fr); max-width: 400px; margin-top: var(--spacing-md);">
|
||
<button type="button" class="stat-card filter-card ai-action" onclick="reevaluateLowScores()" id="aiReevalLowBtn" title="Re-ewaluacja newsów z oceną 1-2★ zawierających nowe tematy (Via Pomerania, NORDA, S6...)">
|
||
<div class="stat-value" style="font-size: var(--font-size-xl);">🔄⭐</div>
|
||
<div class="stat-label">Re-ewaluuj niskie oceny (Via Pomerania, NORDA...)</div>
|
||
</button>
|
||
</div>
|
||
|
||
<div id="aiEvalResultLegacy" 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 (ZOP Kaszubia 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-phases" id="progressPhases">
|
||
<!-- Phases will be rendered by JS -->
|
||
</div>
|
||
<div class="progress-steps" id="progressSteps"></div>
|
||
</div>
|
||
|
||
<!-- Results Container (shown after completion) -->
|
||
<div class="search-results-container" id="searchResultsContainer" style="display: none;">
|
||
<!-- Summary Stats -->
|
||
<div class="search-results-summary" id="searchResultsSummary"></div>
|
||
|
||
<!-- Auto-approved articles list -->
|
||
<div class="auto-approved-section" id="autoApprovedSection" style="display: none;">
|
||
<h4>✅ Artykuły automatycznie zaakceptowane (3+★)</h4>
|
||
<div class="auto-approved-list" id="autoApprovedList"></div>
|
||
</div>
|
||
|
||
<!-- Detailed Statistics Section -->
|
||
<div class="detailed-stats-section" id="detailedStatsSection" style="margin-top: var(--spacing-lg); display: none;">
|
||
<h4 style="margin-bottom: var(--spacing-sm); font-size: var(--font-size-sm); color: var(--text-secondary);">
|
||
📊 Szczegóły procesu
|
||
</h4>
|
||
<div class="detailed-stats-grid" id="detailedStatsGrid" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: var(--spacing-sm);"></div>
|
||
</div>
|
||
|
||
<!-- AI Rejected Articles Section -->
|
||
<div class="ai-rejected-section" id="aiRejectedSection" style="display: none; margin-top: var(--spacing-lg);">
|
||
<h4 style="color: #ef4444;">❌ Artykuły odrzucone przez AI</h4>
|
||
<div class="ai-rejected-list" id="aiRejectedList" style="max-height: 200px; overflow-y: auto;"></div>
|
||
</div>
|
||
|
||
<!-- OK Button to close -->
|
||
<div class="results-actions" style="margin-top: var(--spacing-xl); text-align: center; padding-top: var(--spacing-lg); border-top: 1px solid var(--border);">
|
||
<button type="button" class="btn btn-primary btn-lg" onclick="location.reload()" style="padding: var(--spacing-md) var(--spacing-2xl); font-size: var(--font-size-lg);">
|
||
✓ OK - Zamknij i odśwież
|
||
</button>
|
||
<p style="margin-top: var(--spacing-sm); font-size: var(--font-size-xs); color: var(--text-secondary);">
|
||
Kliknij aby zamknąć okno wyników i odświeżyć listę newsów
|
||
</p>
|
||
</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 with star rating #}
|
||
{% if news.ai_relevant is not none %}
|
||
<span class="ai-badge score-{{ news.ai_relevance_score or 3 }}" title="{{ news.ai_evaluation_reason or '' }}">
|
||
🤖
|
||
<span class="ai-stars">
|
||
{% for i in range(1, 6) %}
|
||
<span class="{{ 'star-filled' if i <= (news.ai_relevance_score or 0) else 'star-empty' }}">★</span>
|
||
{% endfor %}
|
||
</span>
|
||
</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>
|
||
|
||
<!-- AI Knowledge Base -->
|
||
<div class="panel-section">
|
||
<h2>🧠 Baza Wiedzy AI</h2>
|
||
|
||
<div id="knowledge-stats-loading" class="text-center py-4">
|
||
<span class="spinner"></span> Ładowanie statystyk...
|
||
</div>
|
||
|
||
<div id="knowledge-stats-content" style="display: none;">
|
||
<!-- Content Scraping Stats -->
|
||
<div class="stats-section">
|
||
<h3 class="stats-section-title">Scraping treści</h3>
|
||
<div class="stats-grid stats-grid-small">
|
||
<div class="stat-card info-only">
|
||
<div class="stat-value" id="kb-total-approved">-</div>
|
||
<div class="stat-label">Zatwierdzonych newsów</div>
|
||
</div>
|
||
<div class="stat-card info-only success">
|
||
<div class="stat-value" id="kb-scraped">-</div>
|
||
<div class="stat-label">Zescrapowanych</div>
|
||
</div>
|
||
<div class="stat-card info-only warning">
|
||
<div class="stat-value" id="kb-pending-scrape">-</div>
|
||
<div class="stat-label">Oczekuje na scraping</div>
|
||
</div>
|
||
<button class="stat-card filter-card ai-action" onclick="scrapeContent()" id="scrapeBtn" title="Scrapuj treść artykułów">
|
||
<div class="stat-value" style="font-size: var(--font-size-xl);">📄</div>
|
||
<div class="stat-label">Scrapuj treść</div>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Knowledge Extraction Stats -->
|
||
<div class="stats-section">
|
||
<h3 class="stats-section-title">Ekstrakcja wiedzy (Gemini AI)</h3>
|
||
<div class="stats-grid stats-grid-small">
|
||
<div class="stat-card info-only">
|
||
<div class="stat-value" id="kb-total-chunks">-</div>
|
||
<div class="stat-label">Chunks</div>
|
||
</div>
|
||
<div class="stat-card info-only">
|
||
<div class="stat-value" id="kb-total-facts">-</div>
|
||
<div class="stat-label">Faktów</div>
|
||
</div>
|
||
<div class="stat-card info-only">
|
||
<div class="stat-value" id="kb-total-entities">-</div>
|
||
<div class="stat-label">Encji</div>
|
||
</div>
|
||
<button class="stat-card filter-card ai-action" onclick="extractKnowledge()" id="extractBtn" title="Wyekstrahuj wiedzę z zescrapowanych artykułów">
|
||
<div class="stat-value" style="font-size: var(--font-size-xl);">🤖</div>
|
||
<div class="stat-label">Ekstrakcja AI</div>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Embeddings Stats -->
|
||
<div class="stats-section">
|
||
<h3 class="stats-section-title">Semantic Search (Embeddings)</h3>
|
||
<div class="stats-grid stats-grid-small">
|
||
<div class="stat-card info-only success">
|
||
<div class="stat-value" id="kb-with-embeddings">-</div>
|
||
<div class="stat-label">Z embeddingami</div>
|
||
</div>
|
||
<div class="stat-card info-only warning">
|
||
<div class="stat-value" id="kb-pending-embeddings">-</div>
|
||
<div class="stat-label">Bez embeddingów</div>
|
||
</div>
|
||
<button class="stat-card filter-card ai-action" onclick="generateEmbeddings()" id="embeddingsBtn" title="Wygeneruj embeddingi dla semantic search">
|
||
<div class="stat-value" style="font-size: var(--font-size-xl);">🔍</div>
|
||
<div class="stat-label">Generuj embeddingi</div>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Top Entities -->
|
||
<div class="stats-section" id="top-entities-section" style="display: none;">
|
||
<h3 class="stats-section-title">Najczęściej wymieniane encje</h3>
|
||
<div id="top-entities-list" class="entity-pills"></div>
|
||
</div>
|
||
|
||
<!-- Link to detailed dashboard -->
|
||
<div class="stats-section" style="text-align: center; padding-top: var(--spacing-lg);">
|
||
<a href="{{ url_for('admin_zopk_knowledge_dashboard') }}" class="btn btn-primary">
|
||
📊 Szczegółowy panel bazy wiedzy →
|
||
</a>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="knowledge-stats-error" style="display: none;" class="alert alert-danger">
|
||
Błąd ładowania statystyk
|
||
</div>
|
||
</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 %}
|
||
|
||
<!-- AI Evaluation Modal -->
|
||
<div class="modal-overlay" id="aiEvalModal">
|
||
<div class="modal">
|
||
<div id="aiEvalConfirm">
|
||
<div class="modal-icon">🤖</div>
|
||
<h3>Ocena AI (Gemini)</h3>
|
||
<p class="modal-description">Czy chcesz uruchomić ocenę AI dla <strong>max 20</strong> z {{ stats.ai_not_evaluated }} nieocenionych newsów?</p>
|
||
<p class="modal-note">Gemini oceni każdy news pod kątem związku z ZOP Kaszubia.<br>
|
||
<strong>Uwaga:</strong> Proces trwa ok. 30-60 sekund (każdy news = osobne zapytanie do AI).</p>
|
||
<div class="modal-actions">
|
||
<button type="button" class="btn btn-secondary" onclick="closeAiEvalModal()">Anuluj</button>
|
||
<button type="button" class="btn btn-primary" onclick="startAiEvaluation()">
|
||
<span>🚀</span> Rozpocznij ocenę
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div id="aiEvalProgress" style="display: none;">
|
||
<div class="modal-icon spinning">⚙️</div>
|
||
<h3>Trwa ocena AI...</h3>
|
||
<p class="modal-description" id="aiEvalStatusText">Analizowanie newsów przez Gemini...</p>
|
||
<div class="ai-progress-bar">
|
||
<div class="ai-progress-fill" id="aiProgressFill"></div>
|
||
</div>
|
||
<p class="modal-note" id="aiEvalCounter">Proszę czekać...</p>
|
||
</div>
|
||
<div id="aiEvalResult" style="display: none;">
|
||
<div class="modal-icon" id="aiResultIcon">✓</div>
|
||
<h3 id="aiResultTitle">Ocena zakończona</h3>
|
||
<div class="ai-result-stats" id="aiResultStats"></div>
|
||
<div class="modal-actions">
|
||
<button type="button" class="btn btn-primary" onclick="closeAiEvalModal(); location.reload();">Zamknij i odśwież</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- AI Operations Progress Modal -->
|
||
<div class="modal-overlay" id="aiOpsModal">
|
||
<div class="modal modal-wide">
|
||
<div class="modal-header">
|
||
<div class="modal-icon" id="aiOpsIcon">⏳</div>
|
||
<h3 id="aiOpsTitle">Operacja w toku...</h3>
|
||
<button type="button" class="modal-close-btn" id="aiOpsCloseBtn" style="display: none;" onclick="closeAiOpsModal()">×</button>
|
||
</div>
|
||
|
||
<!-- Progress Section -->
|
||
<div class="ai-ops-progress">
|
||
<div class="ai-progress-bar">
|
||
<div class="ai-progress-fill" id="aiOpsProgressFill" style="width: 0%;"></div>
|
||
</div>
|
||
<div class="ai-ops-stats">
|
||
<span id="aiOpsPercent">0%</span>
|
||
<span id="aiOpsCounter">0 / 0</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Current Operation -->
|
||
<div class="ai-ops-current" id="aiOpsCurrentOp">
|
||
<span class="spinner-small"></span>
|
||
<span id="aiOpsCurrentText">Inicjalizacja...</span>
|
||
</div>
|
||
|
||
<!-- Live Log (scrollable) -->
|
||
<div class="ai-ops-log-container">
|
||
<div class="ai-ops-log-header">
|
||
<span>📋 Log operacji</span>
|
||
<span id="aiOpsLogCount">0 wpisów</span>
|
||
</div>
|
||
<div class="ai-ops-log" id="aiOpsLog">
|
||
<!-- Log entries will be added here -->
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Summary Stats (shown on complete) -->
|
||
<div class="ai-ops-summary" id="aiOpsSummary" style="display: none;">
|
||
<div class="summary-row">
|
||
<span class="summary-label">✓ Sukces:</span>
|
||
<span class="summary-value" id="aiOpsSummarySuccess">0</span>
|
||
</div>
|
||
<div class="summary-row">
|
||
<span class="summary-label">✗ Błędy:</span>
|
||
<span class="summary-value" id="aiOpsSummaryFailed">0</span>
|
||
</div>
|
||
<div class="summary-row" id="aiOpsSummarySkippedRow" style="display: none;">
|
||
<span class="summary-label">⊘ Pominięto:</span>
|
||
<span class="summary-value" id="aiOpsSummarySkipped">0</span>
|
||
</div>
|
||
<div class="summary-row">
|
||
<span class="summary-label">⏱️ Czas:</span>
|
||
<span class="summary-value" id="aiOpsSummaryTime">0s</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Actions -->
|
||
<div class="modal-actions" id="aiOpsActions" style="display: none;">
|
||
<button type="button" class="btn btn-primary" onclick="closeAiOpsModal(); loadKnowledgeStats();">Zamknij i odśwież</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<style>
|
||
/* AI Operations Modal Styles */
|
||
.modal-wide {
|
||
max-width: 700px;
|
||
width: 95%;
|
||
}
|
||
|
||
.modal-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
margin-bottom: 20px;
|
||
position: relative;
|
||
}
|
||
|
||
.modal-header h3 {
|
||
margin: 0;
|
||
flex: 1;
|
||
}
|
||
|
||
.modal-close-btn {
|
||
position: absolute;
|
||
right: 0;
|
||
top: 0;
|
||
background: none;
|
||
border: none;
|
||
font-size: 24px;
|
||
cursor: pointer;
|
||
color: var(--text-muted);
|
||
padding: 5px 10px;
|
||
}
|
||
|
||
.modal-close-btn:hover {
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.ai-ops-progress {
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.ai-ops-stats {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
font-size: 14px;
|
||
margin-top: 8px;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
#aiOpsPercent {
|
||
font-weight: 600;
|
||
color: var(--color-primary);
|
||
}
|
||
|
||
.ai-ops-current {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 12px 15px;
|
||
background: var(--bg-secondary);
|
||
border-radius: 8px;
|
||
margin-bottom: 15px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.ai-ops-log-container {
|
||
border: 1px solid var(--border-color);
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.ai-ops-log-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
padding: 8px 12px;
|
||
background: var(--bg-tertiary);
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
border-bottom: 1px solid var(--border-color);
|
||
}
|
||
|
||
.ai-ops-log {
|
||
height: 250px;
|
||
overflow-y: auto;
|
||
padding: 10px;
|
||
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
||
font-size: 12px;
|
||
background: var(--bg-primary);
|
||
}
|
||
|
||
.log-entry {
|
||
padding: 4px 8px;
|
||
margin: 2px 0;
|
||
border-radius: 4px;
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 8px;
|
||
}
|
||
|
||
.log-entry.processing {
|
||
background: rgba(59, 130, 246, 0.1);
|
||
color: #3b82f6;
|
||
}
|
||
|
||
.log-entry.success {
|
||
background: rgba(34, 197, 94, 0.1);
|
||
color: #22c55e;
|
||
}
|
||
|
||
.log-entry.failed {
|
||
background: rgba(239, 68, 68, 0.1);
|
||
color: #ef4444;
|
||
}
|
||
|
||
.log-entry.skipped {
|
||
background: rgba(234, 179, 8, 0.1);
|
||
color: #eab308;
|
||
}
|
||
|
||
.log-entry.complete {
|
||
background: rgba(168, 85, 247, 0.1);
|
||
color: #a855f7;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.log-time {
|
||
color: var(--text-muted);
|
||
font-size: 11px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.log-message {
|
||
flex: 1;
|
||
word-break: break-word;
|
||
}
|
||
|
||
.ai-ops-summary {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||
gap: 10px;
|
||
padding: 15px;
|
||
background: var(--bg-secondary);
|
||
border-radius: 8px;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.summary-row {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
|
||
.summary-label {
|
||
font-size: 12px;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.summary-value {
|
||
font-size: 24px;
|
||
font-weight: 700;
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
/* Spinning icon animation */
|
||
@keyframes spin {
|
||
from { transform: rotate(0deg); }
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
|
||
.modal-icon.spinning {
|
||
animation: spin 2s linear infinite;
|
||
}
|
||
|
||
/* Responsive log height */
|
||
@media (max-height: 800px) {
|
||
.ai-ops-log {
|
||
height: 180px;
|
||
}
|
||
}
|
||
</style>
|
||
|
||
<!-- 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>
|
||
|
||
<!-- Universal Confirm/Alert Modal -->
|
||
<div class="modal-overlay" id="confirmModal">
|
||
<div class="modal" style="max-width: 420px;">
|
||
<div style="text-align: center; margin-bottom: var(--spacing-lg);">
|
||
<div class="modal-icon" id="confirmModalIcon">❓</div>
|
||
<h3 id="confirmModalTitle" style="margin-bottom: var(--spacing-sm);">Potwierdzenie</h3>
|
||
<p class="modal-description" id="confirmModalMessage">Czy na pewno chcesz kontynuować?</p>
|
||
</div>
|
||
<!-- Optional input for prompt -->
|
||
<div class="form-group" id="confirmModalInputGroup" style="display: none;">
|
||
<label id="confirmModalInputLabel">Wprowadź wartość:</label>
|
||
<input type="text" id="confirmModalInput" placeholder="">
|
||
</div>
|
||
<div class="modal-actions" style="justify-content: center;">
|
||
<button type="button" class="btn btn-secondary" id="confirmModalCancel">Anuluj</button>
|
||
<button type="button" class="btn btn-primary" id="confirmModalOk">OK</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Toast notifications -->
|
||
<div id="toastContainer" style="position: fixed; top: 80px; right: 20px; z-index: 1100; display: flex; flex-direction: column; gap: 10px;"></div>
|
||
|
||
<style>
|
||
/* Toast styles */
|
||
.toast {
|
||
padding: 12px 20px;
|
||
border-radius: var(--radius);
|
||
background: var(--surface);
|
||
border-left: 4px solid var(--primary);
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
animation: slideIn 0.3s ease;
|
||
max-width: 350px;
|
||
}
|
||
.toast.success { border-left-color: var(--success); }
|
||
.toast.error { border-left-color: var(--error); }
|
||
.toast.warning { border-left-color: #f59e0b; }
|
||
.toast-icon { font-size: 1.2em; }
|
||
.toast-message { flex: 1; }
|
||
@keyframes slideIn {
|
||
from { transform: translateX(100%); opacity: 0; }
|
||
to { transform: translateX(0); opacity: 1; }
|
||
}
|
||
@keyframes slideOut {
|
||
from { transform: translateX(0); opacity: 1; }
|
||
to { transform: translateX(100%); opacity: 0; }
|
||
}
|
||
|
||
/* Spinner */
|
||
.spinner, .spinner-small {
|
||
display: inline-block;
|
||
border: 3px solid var(--border);
|
||
border-top-color: var(--primary);
|
||
border-radius: 50%;
|
||
animation: spin 0.8s linear infinite;
|
||
}
|
||
.spinner {
|
||
width: 24px;
|
||
height: 24px;
|
||
}
|
||
.spinner-small {
|
||
width: 16px;
|
||
height: 16px;
|
||
border-width: 2px;
|
||
}
|
||
@keyframes spin {
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
|
||
/* Entity Pills */
|
||
.entity-pills {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: var(--spacing-sm);
|
||
}
|
||
.entity-pill {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: var(--spacing-xs);
|
||
padding: var(--spacing-xs) var(--spacing-sm);
|
||
border-radius: var(--radius-full, 9999px);
|
||
font-size: var(--font-size-sm);
|
||
background: var(--surface-secondary, #f5f5f5);
|
||
border: 1px solid var(--border);
|
||
}
|
||
.entity-pill small {
|
||
color: var(--text-muted);
|
||
}
|
||
.entity-pill.entity-company { background: #dbeafe; border-color: #93c5fd; }
|
||
.entity-pill.entity-person { background: #fef3c7; border-color: #fcd34d; }
|
||
.entity-pill.entity-place { background: #d1fae5; border-color: #6ee7b7; }
|
||
.entity-pill.entity-organization { background: #e0e7ff; border-color: #a5b4fc; }
|
||
.entity-pill.entity-project { background: #fce7f3; border-color: #f9a8d4; }
|
||
|
||
/* Text utilities */
|
||
.text-center { text-align: center; }
|
||
.py-4 { padding-top: 1rem; padding-bottom: 1rem; }
|
||
|
||
/* Alert */
|
||
.alert { padding: var(--spacing-md); border-radius: var(--radius); }
|
||
.alert-danger { background: #fef2f2; border: 1px solid #fecaca; color: #dc2626; }
|
||
</style>
|
||
{% endblock %}
|
||
|
||
{% block extra_js %}
|
||
const csrfToken = '{{ csrf_token() }}';
|
||
|
||
// ===========================================
|
||
// Universal Modal System
|
||
// ===========================================
|
||
let confirmModalResolve = null;
|
||
|
||
function showConfirm(message, options = {}) {
|
||
return new Promise((resolve) => {
|
||
confirmModalResolve = resolve;
|
||
const modal = document.getElementById('confirmModal');
|
||
const icon = document.getElementById('confirmModalIcon');
|
||
const title = document.getElementById('confirmModalTitle');
|
||
const msg = document.getElementById('confirmModalMessage');
|
||
const cancelBtn = document.getElementById('confirmModalCancel');
|
||
const okBtn = document.getElementById('confirmModalOk');
|
||
const inputGroup = document.getElementById('confirmModalInputGroup');
|
||
const input = document.getElementById('confirmModalInput');
|
||
|
||
icon.textContent = options.icon || '❓';
|
||
title.textContent = options.title || 'Potwierdzenie';
|
||
msg.innerHTML = message;
|
||
cancelBtn.textContent = options.cancelText || 'Anuluj';
|
||
okBtn.textContent = options.okText || 'OK';
|
||
okBtn.className = 'btn ' + (options.okClass || 'btn-primary');
|
||
|
||
// Show/hide input for prompt mode
|
||
if (options.showInput) {
|
||
inputGroup.style.display = 'block';
|
||
document.getElementById('confirmModalInputLabel').textContent = options.inputLabel || 'Wprowadź wartość:';
|
||
input.value = options.inputValue || '';
|
||
input.placeholder = options.inputPlaceholder || '';
|
||
} else {
|
||
inputGroup.style.display = 'none';
|
||
}
|
||
|
||
// Show/hide cancel button for alert mode
|
||
cancelBtn.style.display = options.alertOnly ? 'none' : '';
|
||
|
||
modal.classList.add('active');
|
||
|
||
if (options.showInput) {
|
||
setTimeout(() => input.focus(), 100);
|
||
}
|
||
});
|
||
}
|
||
|
||
function closeConfirmModal(result) {
|
||
document.getElementById('confirmModal').classList.remove('active');
|
||
if (confirmModalResolve) {
|
||
confirmModalResolve(result);
|
||
confirmModalResolve = null;
|
||
}
|
||
}
|
||
|
||
// Modal button handlers
|
||
document.getElementById('confirmModalOk').addEventListener('click', () => {
|
||
const inputGroup = document.getElementById('confirmModalInputGroup');
|
||
if (inputGroup.style.display !== 'none') {
|
||
closeConfirmModal(document.getElementById('confirmModalInput').value);
|
||
} else {
|
||
closeConfirmModal(true);
|
||
}
|
||
});
|
||
|
||
document.getElementById('confirmModalCancel').addEventListener('click', () => {
|
||
closeConfirmModal(false);
|
||
});
|
||
|
||
document.getElementById('confirmModal').addEventListener('click', (e) => {
|
||
if (e.target.id === 'confirmModal') {
|
||
closeConfirmModal(false);
|
||
}
|
||
});
|
||
|
||
// Keyboard support
|
||
document.getElementById('confirmModal').addEventListener('keydown', (e) => {
|
||
if (e.key === 'Escape') closeConfirmModal(false);
|
||
if (e.key === 'Enter' && document.getElementById('confirmModalInputGroup').style.display === 'none') {
|
||
closeConfirmModal(true);
|
||
}
|
||
});
|
||
|
||
// Toast notification system
|
||
function showToast(message, type = 'info', duration = 4000) {
|
||
const container = document.getElementById('toastContainer');
|
||
const icons = { success: '✓', error: '✕', warning: '⚠', info: 'ℹ' };
|
||
|
||
const toast = document.createElement('div');
|
||
toast.className = `toast ${type}`;
|
||
toast.innerHTML = `
|
||
<span class="toast-icon">${icons[type] || icons.info}</span>
|
||
<span class="toast-message">${message}</span>
|
||
`;
|
||
container.appendChild(toast);
|
||
|
||
setTimeout(() => {
|
||
toast.style.animation = 'slideOut 0.3s ease forwards';
|
||
setTimeout(() => toast.remove(), 300);
|
||
}, duration);
|
||
}
|
||
|
||
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) {
|
||
closeAddNewsModal();
|
||
showToast('News został dodany', 'success');
|
||
setTimeout(() => location.reload(), 1000);
|
||
} else {
|
||
showToast(data.error || 'Wystąpił błąd', 'error');
|
||
}
|
||
} catch (error) {
|
||
showToast('Błąd połączenia: ' + error.message, 'error');
|
||
}
|
||
});
|
||
|
||
async function approveNews(newsId) {
|
||
const confirmed = await showConfirm('Czy na pewno chcesz zatwierdzić ten news?', {
|
||
icon: '✓',
|
||
title: 'Zatwierdź news',
|
||
okText: 'Zatwierdź',
|
||
okClass: 'btn-success'
|
||
});
|
||
if (!confirmed) 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();
|
||
showToast('News został zatwierdzony', 'success');
|
||
} else {
|
||
showToast(data.error || 'Wystąpił błąd', 'error');
|
||
}
|
||
} catch (error) {
|
||
showToast('Błąd połączenia: ' + error.message, 'error');
|
||
}
|
||
}
|
||
|
||
async function rejectNews(newsId) {
|
||
const reason = await showConfirm('Podaj powód odrzucenia (opcjonalnie):', {
|
||
icon: '✕',
|
||
title: 'Odrzuć news',
|
||
showInput: true,
|
||
inputLabel: 'Powód odrzucenia:',
|
||
inputPlaceholder: 'np. Nieistotny artykuł...',
|
||
okText: 'Odrzuć',
|
||
okClass: 'btn-danger'
|
||
});
|
||
if (reason === false) 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();
|
||
showToast('News został odrzucony', 'success');
|
||
} else {
|
||
showToast(data.error || 'Wystąpił błąd', 'error');
|
||
}
|
||
} catch (error) {
|
||
showToast('Błąd połączenia: ' + error.message, 'error');
|
||
}
|
||
}
|
||
|
||
async function rejectOldNews() {
|
||
const minYear = {{ min_year }};
|
||
const confirmed = await showConfirm(
|
||
`Czy na pewno chcesz odrzucić wszystkie newsy sprzed <strong>${minYear}</strong> roku?<br><br>` +
|
||
`<small style="color: var(--text-muted)">ZOP Kaszubia powstał w 2024 roku, więc starsze artykuły najprawdopodobniej nie dotyczą tego projektu.</small>`,
|
||
{
|
||
icon: '🗑️',
|
||
title: 'Odrzuć stare newsy',
|
||
okText: 'Odrzuć wszystkie',
|
||
okClass: 'btn-danger'
|
||
}
|
||
);
|
||
if (!confirmed) {
|
||
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) {
|
||
showToast(data.message, 'success', 3000);
|
||
setTimeout(() => location.reload(), 1500);
|
||
} else {
|
||
showToast(data.error || 'Wystąpił błąd', 'error');
|
||
}
|
||
} catch (error) {
|
||
showToast('Błąd połączenia: ' + error.message, 'error');
|
||
}
|
||
}
|
||
|
||
// AI Evaluation Modal functions
|
||
function evaluateWithAI() {
|
||
// Open modal instead of confirm()
|
||
document.getElementById('aiEvalModal').classList.add('active');
|
||
// Reset modal state
|
||
document.getElementById('aiEvalConfirm').style.display = 'block';
|
||
document.getElementById('aiEvalProgress').style.display = 'none';
|
||
document.getElementById('aiEvalResult').style.display = 'none';
|
||
}
|
||
|
||
function closeAiEvalModal() {
|
||
document.getElementById('aiEvalModal').classList.remove('active');
|
||
}
|
||
|
||
async function startAiEvaluation() {
|
||
const btn = document.getElementById('aiEvalBtn');
|
||
const progressFill = document.getElementById('aiProgressFill');
|
||
const statusText = document.getElementById('aiEvalStatusText');
|
||
const counter = document.getElementById('aiEvalCounter');
|
||
|
||
// Switch to progress view
|
||
document.getElementById('aiEvalConfirm').style.display = 'none';
|
||
document.getElementById('aiEvalProgress').style.display = 'block';
|
||
|
||
// Disable button
|
||
btn.disabled = true;
|
||
btn.querySelector('.stat-label').textContent = 'AI pracuje...';
|
||
|
||
// Animated progress with better messaging
|
||
let progress = 0;
|
||
let seconds = 0;
|
||
const progressInterval = setInterval(() => {
|
||
progress = Math.min(progress + Math.random() * 8, 95);
|
||
seconds++;
|
||
progressFill.style.width = `${progress}%`;
|
||
|
||
// Show elapsed time and encouraging messages
|
||
if (seconds < 30) {
|
||
statusText.textContent = `Gemini analizuje newsy... ${Math.round(progress)}%`;
|
||
} else if (seconds < 60) {
|
||
statusText.textContent = `Prawie gotowe... ${Math.round(progress)}%`;
|
||
} else {
|
||
statusText.textContent = `Jeszcze chwilę... ${Math.round(progress)}%`;
|
||
}
|
||
counter.textContent = `Upłynęło: ${seconds}s (każdy news wymaga wywołania AI)`;
|
||
}, 1000);
|
||
|
||
try {
|
||
// Use AbortController for timeout (3 minutes max)
|
||
const controller = new AbortController();
|
||
const timeoutId = setTimeout(() => controller.abort(), 180000);
|
||
|
||
const response = await fetch('/admin/zopk/news/evaluate-ai', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-CSRFToken': csrfToken
|
||
},
|
||
body: JSON.stringify({ limit: 20 }), // Reduced limit for faster response
|
||
signal: controller.signal
|
||
});
|
||
|
||
clearTimeout(timeoutId);
|
||
clearInterval(progressInterval);
|
||
progressFill.style.width = '100%';
|
||
|
||
const data = await response.json();
|
||
|
||
// Switch to result view
|
||
document.getElementById('aiEvalProgress').style.display = 'none';
|
||
document.getElementById('aiEvalResult').style.display = 'block';
|
||
|
||
const resultIcon = document.getElementById('aiResultIcon');
|
||
const resultTitle = document.getElementById('aiResultTitle');
|
||
const resultStats = document.getElementById('aiResultStats');
|
||
|
||
if (data.success) {
|
||
resultIcon.textContent = '✓';
|
||
resultIcon.className = 'modal-icon success';
|
||
resultTitle.textContent = 'Ocena zakończona!';
|
||
resultStats.innerHTML = `
|
||
<div class="ai-stat-item success">
|
||
<div class="value">${data.relevant_count}</div>
|
||
<div class="label">Pasuje do ZOP Kaszubia</div>
|
||
</div>
|
||
<div class="ai-stat-item danger">
|
||
<div class="value">${data.not_relevant_count}</div>
|
||
<div class="label">Nie pasuje</div>
|
||
</div>
|
||
<div class="ai-stat-item warning">
|
||
<div class="value">${data.total_evaluated}</div>
|
||
<div class="label">Ocenionych</div>
|
||
</div>
|
||
<p style="text-align: center; margin-top: var(--spacing-md); color: var(--text-secondary);">
|
||
Odświeżam stronę za 2 sekundy...
|
||
</p>
|
||
`;
|
||
// Auto-reload after showing results
|
||
setTimeout(() => location.reload(), 2000);
|
||
} else {
|
||
resultIcon.textContent = '✗';
|
||
resultIcon.className = 'modal-icon error';
|
||
resultTitle.textContent = 'Wystąpił błąd';
|
||
resultStats.innerHTML = `<p style="text-align: center; color: var(--danger);">${data.error}</p>`;
|
||
btn.disabled = false;
|
||
btn.querySelector('.stat-label').textContent = 'AI: Oceń ponownie';
|
||
}
|
||
} catch (error) {
|
||
clearInterval(progressInterval);
|
||
|
||
document.getElementById('aiEvalProgress').style.display = 'none';
|
||
document.getElementById('aiEvalResult').style.display = 'block';
|
||
|
||
const resultIcon = document.getElementById('aiResultIcon');
|
||
const resultTitle = document.getElementById('aiResultTitle');
|
||
const resultStats = document.getElementById('aiResultStats');
|
||
|
||
resultIcon.textContent = '✗';
|
||
resultIcon.className = 'modal-icon error';
|
||
|
||
// Check if timeout
|
||
if (error.name === 'AbortError') {
|
||
resultTitle.textContent = 'Przekroczono limit czasu';
|
||
resultStats.innerHTML = `<p style="text-align: center; color: var(--danger);">
|
||
Przetwarzanie trwało zbyt długo (>3 min).<br>
|
||
Spróbuj ponownie lub sprawdź logi serwera.
|
||
</p>`;
|
||
} else {
|
||
resultTitle.textContent = 'Błąd połączenia';
|
||
resultStats.innerHTML = `<p style="text-align: center; color: var(--danger);">${error.message}</p>`;
|
||
}
|
||
|
||
btn.disabled = false;
|
||
btn.querySelector('.stat-label').textContent = 'AI: Oceń ponownie';
|
||
}
|
||
}
|
||
|
||
// Re-evaluate items missing scores (upgrade to 1-5 stars)
|
||
async function reevaluateScores() {
|
||
const btn = document.getElementById('aiRescoreBtn');
|
||
if (!btn) return;
|
||
|
||
// Show modal for re-evaluation
|
||
document.getElementById('aiEvalModal').classList.add('active');
|
||
document.getElementById('aiEvalConfirm').style.display = 'none';
|
||
document.getElementById('aiEvalProgress').style.display = 'block';
|
||
document.getElementById('aiEvalResult').style.display = 'none';
|
||
|
||
const progressBar = document.getElementById('aiProgressFill');
|
||
const progressStatus = document.getElementById('aiProgressStatus');
|
||
|
||
btn.disabled = true;
|
||
btn.querySelector('.stat-label').textContent = 'AI pracuje...';
|
||
|
||
// Simulated progress for better UX
|
||
let progress = 0;
|
||
let startTime = Date.now();
|
||
const progressInterval = setInterval(() => {
|
||
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
||
if (progress < 90) {
|
||
progress += Math.random() * 3;
|
||
progressBar.style.width = Math.min(progress, 90) + '%';
|
||
}
|
||
progressStatus.textContent = `AI dodaje gwiazdki... (${elapsed}s)`;
|
||
}, 500);
|
||
|
||
try {
|
||
const controller = new AbortController();
|
||
const timeoutId = setTimeout(() => controller.abort(), 180000);
|
||
|
||
const response = await fetch('/admin/zopk/news/reevaluate-scores', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-CSRFToken': csrfToken
|
||
},
|
||
body: JSON.stringify({ limit: 30 }),
|
||
signal: controller.signal
|
||
});
|
||
|
||
clearTimeout(timeoutId);
|
||
clearInterval(progressInterval);
|
||
|
||
const data = await response.json();
|
||
|
||
progressBar.style.width = '100%';
|
||
|
||
document.getElementById('aiEvalProgress').style.display = 'none';
|
||
document.getElementById('aiEvalResult').style.display = 'block';
|
||
|
||
const resultIcon = document.getElementById('aiResultIcon');
|
||
const resultTitle = document.getElementById('aiResultTitle');
|
||
const resultStats = document.getElementById('aiResultStats');
|
||
|
||
if (data.success) {
|
||
resultIcon.textContent = '⭐';
|
||
resultIcon.className = 'modal-icon success';
|
||
resultTitle.textContent = 'Gwiazdki dodane!';
|
||
resultStats.innerHTML = `
|
||
<div style="display: flex; gap: 20px; justify-content: center; margin-bottom: var(--spacing-md);">
|
||
<div><strong>${data.total_evaluated}</strong><br><small>Ocenionych</small></div>
|
||
<div style="color: var(--success);"><strong>${data.relevant_count}</strong><br><small>Pasuje</small></div>
|
||
<div style="color: var(--danger);"><strong>${data.not_relevant_count}</strong><br><small>Nie pasuje</small></div>
|
||
</div>
|
||
<p style="text-align: center; color: var(--text-secondary);">${data.message}</p>
|
||
`;
|
||
setTimeout(() => location.reload(), 2000);
|
||
} else {
|
||
resultIcon.textContent = '✗';
|
||
resultIcon.className = 'modal-icon error';
|
||
resultTitle.textContent = 'Wystąpił błąd';
|
||
resultStats.innerHTML = `<p style="text-align: center; color: var(--danger);">${data.error}</p>`;
|
||
btn.disabled = false;
|
||
btn.querySelector('.stat-label').textContent = 'AI: Spróbuj ponownie';
|
||
}
|
||
} catch (error) {
|
||
clearInterval(progressInterval);
|
||
document.getElementById('aiEvalProgress').style.display = 'none';
|
||
document.getElementById('aiEvalResult').style.display = 'block';
|
||
|
||
const resultIcon = document.getElementById('aiResultIcon');
|
||
const resultTitle = document.getElementById('aiResultTitle');
|
||
const resultStats = document.getElementById('aiResultStats');
|
||
|
||
resultIcon.textContent = '✗';
|
||
resultIcon.className = 'modal-icon error';
|
||
resultTitle.textContent = 'Błąd połączenia';
|
||
resultStats.innerHTML = `<p style="text-align: center; color: var(--danger);">${error.message}</p>`;
|
||
|
||
btn.disabled = false;
|
||
btn.querySelector('.stat-label').textContent = 'AI: Spróbuj ponownie';
|
||
}
|
||
}
|
||
|
||
// Re-evaluate low score news (1-2★) with key topics (Via Pomerania, NORDA, etc.)
|
||
async function reevaluateLowScores() {
|
||
const btn = document.getElementById('aiReevalLowBtn');
|
||
if (!btn) return;
|
||
|
||
// Show modal for re-evaluation
|
||
document.getElementById('aiEvalModal').classList.add('active');
|
||
document.getElementById('aiEvalConfirm').style.display = 'none';
|
||
document.getElementById('aiEvalProgress').style.display = 'block';
|
||
document.getElementById('aiEvalResult').style.display = 'none';
|
||
|
||
const progressBar = document.getElementById('aiProgressFill');
|
||
const progressStatus = document.getElementById('aiProgressStatus');
|
||
|
||
btn.disabled = true;
|
||
btn.querySelector('.stat-label').textContent = 'Re-ewaluacja...';
|
||
|
||
// Simulated progress for better UX
|
||
let progress = 0;
|
||
let startTime = Date.now();
|
||
const progressInterval = setInterval(() => {
|
||
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
||
if (progress < 90) {
|
||
progress += Math.random() * 3;
|
||
progressBar.style.width = Math.min(progress, 90) + '%';
|
||
}
|
||
progressStatus.textContent = `Re-ewaluacja newsów z Via Pomerania, NORDA... (${elapsed}s)`;
|
||
}, 500);
|
||
|
||
try {
|
||
const controller = new AbortController();
|
||
const timeoutId = setTimeout(() => controller.abort(), 180000);
|
||
|
||
const response = await fetch('/admin/zopk/news/reevaluate-low-scores', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-CSRFToken': csrfToken
|
||
},
|
||
body: JSON.stringify({ limit: 50 }),
|
||
signal: controller.signal
|
||
});
|
||
|
||
clearTimeout(timeoutId);
|
||
clearInterval(progressInterval);
|
||
|
||
const data = await response.json();
|
||
|
||
progressBar.style.width = '100%';
|
||
|
||
document.getElementById('aiEvalProgress').style.display = 'none';
|
||
document.getElementById('aiEvalResult').style.display = 'block';
|
||
|
||
const resultIcon = document.getElementById('aiResultIcon');
|
||
const resultTitle = document.getElementById('aiResultTitle');
|
||
const resultStats = document.getElementById('aiResultStats');
|
||
|
||
if (data.success) {
|
||
resultIcon.textContent = '🔄';
|
||
resultIcon.className = 'modal-icon success';
|
||
resultTitle.textContent = 'Re-ewaluacja zakończona!';
|
||
|
||
// Build details list if available
|
||
let detailsHtml = '';
|
||
if (data.details && data.details.length > 0) {
|
||
detailsHtml = `
|
||
<div style="max-height: 200px; overflow-y: auto; margin-top: var(--spacing-md); text-align: left; font-size: var(--font-size-sm);">
|
||
${data.details.map(d => `
|
||
<div style="padding: 4px 0; border-bottom: 1px solid var(--border);">
|
||
<span style="color: ${d.change === 'upgraded' ? 'var(--success)' : d.change === 'downgraded' ? 'var(--danger)' : 'var(--text-secondary)'};">
|
||
${d.old_score}★ → ${d.new_score}★
|
||
</span>
|
||
${d.title}...
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
resultStats.innerHTML = `
|
||
<div style="display: flex; gap: 20px; justify-content: center; margin-bottom: var(--spacing-md);">
|
||
<div><strong>${data.total_evaluated}</strong><br><small>Ocenionych</small></div>
|
||
<div style="color: var(--success);"><strong>${data.upgraded}</strong><br><small>⬆️ Podwyższono</small></div>
|
||
<div style="color: var(--danger);"><strong>${data.downgraded}</strong><br><small>⬇️ Obniżono</small></div>
|
||
<div style="color: var(--text-secondary);"><strong>${data.unchanged}</strong><br><small>Bez zmian</small></div>
|
||
</div>
|
||
<p style="text-align: center; color: var(--text-secondary);">${data.message}</p>
|
||
${detailsHtml}
|
||
`;
|
||
|
||
if (data.upgraded > 0 || data.downgraded > 0) {
|
||
setTimeout(() => location.reload(), 3000);
|
||
}
|
||
} else {
|
||
resultIcon.textContent = '✗';
|
||
resultIcon.className = 'modal-icon error';
|
||
resultTitle.textContent = 'Wystąpił błąd';
|
||
resultStats.innerHTML = `<p style="text-align: center; color: var(--danger);">${data.error}</p>`;
|
||
btn.disabled = false;
|
||
btn.querySelector('.stat-label').textContent = 'Re-ewaluuj niskie oceny';
|
||
}
|
||
} catch (error) {
|
||
clearInterval(progressInterval);
|
||
document.getElementById('aiEvalProgress').style.display = 'none';
|
||
document.getElementById('aiEvalResult').style.display = 'block';
|
||
|
||
const resultIcon = document.getElementById('aiResultIcon');
|
||
const resultTitle = document.getElementById('aiResultTitle');
|
||
const resultStats = document.getElementById('aiResultStats');
|
||
|
||
resultIcon.textContent = '✗';
|
||
resultIcon.className = 'modal-icon error';
|
||
resultTitle.textContent = 'Błąd połączenia';
|
||
resultStats.innerHTML = `<p style="text-align: center; color: var(--danger);">${error.message}</p>`;
|
||
|
||
btn.disabled = false;
|
||
btn.querySelector('.stat-label').textContent = 'Re-ewaluuj niskie oceny';
|
||
}
|
||
}
|
||
|
||
// Close modal on click outside
|
||
document.getElementById('aiEvalModal').addEventListener('click', function(e) {
|
||
if (e.target === this) {
|
||
closeAiEvalModal();
|
||
}
|
||
});
|
||
|
||
// 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 (ZOP Kaszubia)',
|
||
'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 progressContainer = document.getElementById('progressContainer');
|
||
const progressBar = document.getElementById('progressBar');
|
||
const progressStatus = document.getElementById('progressStatus');
|
||
const progressPercent = document.getElementById('progressPercent');
|
||
const progressPhases = document.getElementById('progressPhases');
|
||
const progressSteps = document.getElementById('progressSteps');
|
||
const resultsContainer = document.getElementById('searchResultsContainer');
|
||
const resultsSummary = document.getElementById('searchResultsSummary');
|
||
const autoApprovedSection = document.getElementById('autoApprovedSection');
|
||
const autoApprovedList = document.getElementById('autoApprovedList');
|
||
const query = document.getElementById('searchQuery').value;
|
||
|
||
// Process phases definition
|
||
const PHASES = [
|
||
{ id: 'search', icon: '🔍', label: 'Wyszukiwanie' },
|
||
{ id: 'filter', icon: '🚫', label: 'Filtrowanie' },
|
||
{ id: 'ai', icon: '🤖', label: 'Analiza AI' },
|
||
{ id: 'save', icon: '💾', label: 'Zapisywanie' }
|
||
];
|
||
|
||
// Reset UI
|
||
btn.disabled = true;
|
||
btn.textContent = 'Szukam...';
|
||
resultsContainer.style.display = 'none';
|
||
autoApprovedSection.style.display = 'none';
|
||
progressContainer.classList.add('active');
|
||
progressBar.style.width = '0%';
|
||
progressBar.style.background = ''; // Reset color
|
||
progressPercent.textContent = '0%';
|
||
|
||
// Build progress phases UI
|
||
progressPhases.innerHTML = PHASES.map(phase => `
|
||
<div class="progress-phase pending" id="phase-${phase.id}">
|
||
<span class="progress-phase-icon">${phase.icon}</span>
|
||
<span>${phase.label}</span>
|
||
</div>
|
||
`).join('');
|
||
|
||
// Build initial progress steps (will be populated from process_log)
|
||
progressSteps.innerHTML = '<div class="progress-step active"><span class="progress-step-icon">⏳</span><span>Inicjalizacja...</span></div>';
|
||
|
||
// Simulate progress phases while waiting for API
|
||
let currentPhaseIdx = 0;
|
||
const phaseMessages = [
|
||
'Przeszukuję źródła (Brave API + RSS)...',
|
||
'Filtruję wyniki (blacklist, słowa kluczowe)...',
|
||
'Analiza AI (Gemini ocenia artykuły)...',
|
||
'Zapisuję do bazy wiedzy...'
|
||
];
|
||
|
||
const progressInterval = setInterval(() => {
|
||
if (currentPhaseIdx < PHASES.length) {
|
||
// Update phase UI
|
||
PHASES.forEach((phase, idx) => {
|
||
const el = document.getElementById(`phase-${phase.id}`);
|
||
if (el) {
|
||
el.classList.remove('pending', 'active', 'completed');
|
||
if (idx < currentPhaseIdx) el.classList.add('completed');
|
||
else if (idx === currentPhaseIdx) el.classList.add('active');
|
||
else el.classList.add('pending');
|
||
}
|
||
});
|
||
|
||
progressStatus.textContent = phaseMessages[currentPhaseIdx];
|
||
const percent = Math.round(((currentPhaseIdx + 1) / PHASES.length) * 80);
|
||
progressBar.style.width = `${percent}%`;
|
||
progressPercent.textContent = `${percent}%`;
|
||
|
||
currentPhaseIdx++;
|
||
}
|
||
}, 2500); // Each phase ~2.5s for realistic timing
|
||
|
||
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();
|
||
|
||
if (data.success) {
|
||
// Mark all phases as completed
|
||
PHASES.forEach(phase => {
|
||
const el = document.getElementById(`phase-${phase.id}`);
|
||
if (el) {
|
||
el.classList.remove('pending', 'active');
|
||
el.classList.add('completed');
|
||
}
|
||
});
|
||
|
||
progressBar.style.width = '100%';
|
||
progressPercent.textContent = '100%';
|
||
progressStatus.textContent = '✅ Wyszukiwanie zakończone!';
|
||
|
||
// Display process log as steps
|
||
if (data.process_log && data.process_log.length > 0) {
|
||
// Show last few important steps
|
||
const importantSteps = data.process_log.filter(log =>
|
||
log.step.includes('done') || log.step.includes('complete') || log.phase === 'complete'
|
||
).slice(-6);
|
||
|
||
progressSteps.innerHTML = importantSteps.map(log => `
|
||
<div class="progress-step completed">
|
||
<span class="progress-step-icon">✓</span>
|
||
<span>${log.message}</span>
|
||
${log.count > 0 ? `<span class="progress-step-count">${log.count}</span>` : ''}
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
// Hide progress container after a moment
|
||
setTimeout(() => {
|
||
progressContainer.classList.remove('active');
|
||
}, 1500);
|
||
|
||
// Show results container
|
||
resultsContainer.style.display = 'block';
|
||
|
||
// Build summary stats
|
||
resultsSummary.innerHTML = `
|
||
<div class="summary-stat info">
|
||
<div class="value">${data.total_found || 0}</div>
|
||
<div class="label">Znaleziono</div>
|
||
</div>
|
||
<div class="summary-stat warning">
|
||
<div class="value">${(data.blacklisted || 0) + (data.keyword_filtered || 0)}</div>
|
||
<div class="label">Odfiltrowano</div>
|
||
</div>
|
||
<div class="summary-stat error">
|
||
<div class="value">${data.ai_rejected || 0}</div>
|
||
<div class="label">AI odrzucił</div>
|
||
</div>
|
||
<div class="summary-stat success">
|
||
<div class="value">${data.ai_approved || 0}</div>
|
||
<div class="label">AI zaakceptował</div>
|
||
</div>
|
||
<div class="summary-stat success">
|
||
<div class="value">${data.saved_new || 0}</div>
|
||
<div class="label">Nowe w bazie</div>
|
||
</div>
|
||
`;
|
||
|
||
// Show auto-approved articles list
|
||
if (data.auto_approved_articles && data.auto_approved_articles.length > 0) {
|
||
autoApprovedSection.style.display = 'block';
|
||
autoApprovedList.innerHTML = data.auto_approved_articles.map(article => {
|
||
const stars = '★'.repeat(article.score) + '☆'.repeat(5 - article.score);
|
||
return `
|
||
<div class="auto-approved-item">
|
||
<span class="stars">${stars}</span>
|
||
<span class="title">${article.title}</span>
|
||
<span class="source">${article.source || ''}</span>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
// Show detailed statistics section
|
||
const detailedStatsSection = document.getElementById('detailedStatsSection');
|
||
const detailedStatsGrid = document.getElementById('detailedStatsGrid');
|
||
const aiRejectedSection = document.getElementById('aiRejectedSection');
|
||
const aiRejectedList = document.getElementById('aiRejectedList');
|
||
|
||
// Build detailed statistics
|
||
const detailedStats = [
|
||
{ label: 'Zapytanie', value: query || 'ZOP Kaszubia', icon: '🔍', type: 'info' },
|
||
{ label: 'Źródła przeszukane', value: `Brave API + RSS`, icon: '📡', type: 'info' },
|
||
{ label: 'Łącznie znaleziono', value: data.total_found || 0, icon: '📰', type: 'info' },
|
||
{ label: 'Zablokowane (blacklist)', value: data.blacklisted || 0, icon: '🚫', type: 'warning' },
|
||
{ label: 'Odfiltrowane (słowa kluczowe)', value: data.keyword_filtered || 0, icon: '🔤', type: 'warning' },
|
||
{ label: 'Przekazane do AI', value: data.sent_to_ai || 0, icon: '🤖', type: 'info' },
|
||
{ label: 'AI zaakceptował (3+★)', value: data.ai_approved || 0, icon: '✅', type: 'success' },
|
||
{ label: 'AI odrzucił (1-2★)', value: data.ai_rejected || 0, icon: '❌', type: 'error' },
|
||
{ label: 'Duplikaty (już w bazie)', value: data.duplicates || 0, icon: '🔄', type: 'info' },
|
||
{ label: 'Zapisano nowych', value: data.saved_new || 0, icon: '💾', type: 'success' },
|
||
{ label: 'Do bazy wiedzy', value: data.knowledge_entities_created || 0, icon: '🧠', type: 'success' },
|
||
{ label: 'Czas przetwarzania', value: data.processing_time ? `${data.processing_time.toFixed(1)}s` : '-', icon: '⏱️', type: 'info' }
|
||
];
|
||
|
||
detailedStatsGrid.innerHTML = detailedStats.map(stat => `
|
||
<div class="detailed-stat-item" style="
|
||
background: ${stat.type === 'success' ? '#dcfce7' : stat.type === 'error' ? '#fee2e2' : stat.type === 'warning' ? '#fef3c7' : '#f3f4f6'};
|
||
padding: var(--spacing-sm) var(--spacing-md);
|
||
border-radius: var(--radius);
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
">
|
||
<span style="display: flex; align-items: center; gap: var(--spacing-xs);">
|
||
<span>${stat.icon}</span>
|
||
<span style="font-size: var(--font-size-sm);">${stat.label}</span>
|
||
</span>
|
||
<strong style="color: ${stat.type === 'success' ? '#166534' : stat.type === 'error' ? '#991b1b' : stat.type === 'warning' ? '#92400e' : '#374151'};">
|
||
${stat.value}
|
||
</strong>
|
||
</div>
|
||
`).join('');
|
||
detailedStatsSection.style.display = 'block';
|
||
|
||
// Show AI rejected articles if any
|
||
if (data.ai_rejected_articles && data.ai_rejected_articles.length > 0) {
|
||
aiRejectedSection.style.display = 'block';
|
||
aiRejectedList.innerHTML = data.ai_rejected_articles.map(article => {
|
||
const stars = '★'.repeat(article.score || 1) + '☆'.repeat(5 - (article.score || 1));
|
||
return `
|
||
<div class="ai-rejected-item" style="
|
||
padding: var(--spacing-xs) var(--spacing-sm);
|
||
border-bottom: 1px solid #fee2e2;
|
||
font-size: var(--font-size-sm);
|
||
display: flex;
|
||
gap: var(--spacing-sm);
|
||
align-items: center;
|
||
">
|
||
<span style="color: #f59e0b;">${stars}</span>
|
||
<span style="flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${article.title}</span>
|
||
<span style="color: var(--text-secondary); font-size: var(--font-size-xs);">${article.source || ''}</span>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
// Scroll to results for better visibility
|
||
resultsContainer.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||
|
||
} else {
|
||
// Error handling
|
||
progressBar.style.width = '100%';
|
||
progressBar.style.background = '#fca5a5';
|
||
progressStatus.textContent = 'Błąd wyszukiwania';
|
||
|
||
PHASES.forEach(phase => {
|
||
const el = document.getElementById(`phase-${phase.id}`);
|
||
if (el) el.classList.remove('active');
|
||
});
|
||
|
||
progressSteps.innerHTML = `
|
||
<div class="progress-step" style="color: #fca5a5;">
|
||
<span class="progress-step-icon">✗</span>
|
||
<span>Błąd: ${data.error}</span>
|
||
</div>
|
||
`;
|
||
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';
|
||
|
||
progressSteps.innerHTML = `
|
||
<div class="progress-step" style="color: #fca5a5;">
|
||
<span class="progress-step-icon">✗</span>
|
||
<span>Błąd połączenia: ${error.message}</span>
|
||
</div>
|
||
`;
|
||
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();
|
||
}
|
||
});
|
||
|
||
// ===========================================
|
||
// AI Knowledge Base Functions
|
||
// ===========================================
|
||
|
||
async function loadKnowledgeStats() {
|
||
try {
|
||
const response = await fetch('/admin/zopk/knowledge/stats');
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
document.getElementById('knowledge-stats-loading').style.display = 'none';
|
||
document.getElementById('knowledge-stats-content').style.display = 'block';
|
||
|
||
// Scraping stats
|
||
document.getElementById('kb-total-approved').textContent = data.articles?.total_approved || 0;
|
||
document.getElementById('kb-scraped').textContent = data.articles?.scraped || 0;
|
||
document.getElementById('kb-pending-scrape').textContent = data.articles?.pending_scrape || 0;
|
||
|
||
// Knowledge base stats
|
||
document.getElementById('kb-total-chunks').textContent = data.knowledge_base?.total_chunks || 0;
|
||
document.getElementById('kb-total-facts').textContent = data.knowledge_base?.total_facts || 0;
|
||
document.getElementById('kb-total-entities').textContent = data.knowledge_base?.total_entities || 0;
|
||
|
||
// Embeddings stats
|
||
document.getElementById('kb-with-embeddings').textContent = data.knowledge_base?.chunks_with_embeddings || 0;
|
||
document.getElementById('kb-pending-embeddings').textContent = data.knowledge_base?.chunks_without_embeddings || 0;
|
||
|
||
// Top entities
|
||
if (data.top_entities && data.top_entities.length > 0) {
|
||
document.getElementById('top-entities-section').style.display = 'block';
|
||
const list = document.getElementById('top-entities-list');
|
||
list.innerHTML = data.top_entities.map(e =>
|
||
`<span class="entity-pill entity-${e.type}">${e.name} <small>(${e.mentions})</small></span>`
|
||
).join('');
|
||
}
|
||
} else {
|
||
throw new Error(data.error || 'Unknown error');
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading knowledge stats:', error);
|
||
document.getElementById('knowledge-stats-loading').style.display = 'none';
|
||
document.getElementById('knowledge-stats-error').style.display = 'block';
|
||
document.getElementById('knowledge-stats-error').textContent = `Błąd: ${error.message}`;
|
||
}
|
||
}
|
||
|
||
// ===========================================
|
||
// AI Operations Modal Functions
|
||
// ===========================================
|
||
|
||
let aiOpsEventSource = null;
|
||
let aiOpsLogEntries = 0;
|
||
|
||
function openAiOpsModal(title, icon) {
|
||
const modal = document.getElementById('aiOpsModal');
|
||
document.getElementById('aiOpsTitle').textContent = title;
|
||
document.getElementById('aiOpsIcon').textContent = icon;
|
||
document.getElementById('aiOpsIcon').classList.add('spinning');
|
||
|
||
// Reset state
|
||
document.getElementById('aiOpsProgressFill').style.width = '0%';
|
||
document.getElementById('aiOpsPercent').textContent = '0%';
|
||
document.getElementById('aiOpsCounter').textContent = '0 / 0';
|
||
document.getElementById('aiOpsCurrentText').textContent = 'Inicjalizacja...';
|
||
document.getElementById('aiOpsLog').innerHTML = '';
|
||
document.getElementById('aiOpsSummary').style.display = 'none';
|
||
document.getElementById('aiOpsActions').style.display = 'none';
|
||
document.getElementById('aiOpsCloseBtn').style.display = 'none';
|
||
document.getElementById('aiOpsCurrentOp').style.display = 'flex';
|
||
aiOpsLogEntries = 0;
|
||
document.getElementById('aiOpsLogCount').textContent = '0 wpisów';
|
||
|
||
modal.classList.add('active');
|
||
}
|
||
|
||
function closeAiOpsModal() {
|
||
const modal = document.getElementById('aiOpsModal');
|
||
modal.classList.remove('active');
|
||
|
||
// Close SSE connection if active
|
||
if (aiOpsEventSource) {
|
||
aiOpsEventSource.close();
|
||
aiOpsEventSource = null;
|
||
}
|
||
}
|
||
|
||
function addAiOpsLogEntry(status, message) {
|
||
const log = document.getElementById('aiOpsLog');
|
||
const time = new Date().toLocaleTimeString('pl-PL', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||
|
||
const entry = document.createElement('div');
|
||
entry.className = `log-entry ${status}`;
|
||
entry.innerHTML = `
|
||
<span class="log-time">${time}</span>
|
||
<span class="log-message">${message}</span>
|
||
`;
|
||
|
||
log.appendChild(entry);
|
||
log.scrollTop = log.scrollHeight;
|
||
|
||
aiOpsLogEntries++;
|
||
document.getElementById('aiOpsLogCount').textContent = `${aiOpsLogEntries} wpisów`;
|
||
}
|
||
|
||
function updateAiOpsProgress(data) {
|
||
// Update progress bar
|
||
if (data.percent !== undefined) {
|
||
document.getElementById('aiOpsProgressFill').style.width = `${data.percent}%`;
|
||
document.getElementById('aiOpsPercent').textContent = `${Math.round(data.percent)}%`;
|
||
}
|
||
|
||
// Update counter
|
||
if (data.current !== undefined && data.total !== undefined) {
|
||
document.getElementById('aiOpsCounter').textContent = `${data.current} / ${data.total}`;
|
||
}
|
||
|
||
// Update current operation text
|
||
if (data.message) {
|
||
document.getElementById('aiOpsCurrentText').textContent = data.message;
|
||
}
|
||
|
||
// Add log entry
|
||
if (data.status && data.message) {
|
||
addAiOpsLogEntry(data.status, data.message);
|
||
}
|
||
}
|
||
|
||
function completeAiOpsModal(data) {
|
||
// Stop spinning
|
||
document.getElementById('aiOpsIcon').classList.remove('spinning');
|
||
document.getElementById('aiOpsIcon').textContent = data.status === 'error' ? '❌' : '✅';
|
||
document.getElementById('aiOpsTitle').textContent = data.status === 'error' ? 'Błąd operacji' : 'Operacja zakończona';
|
||
|
||
// Hide current operation
|
||
document.getElementById('aiOpsCurrentOp').style.display = 'none';
|
||
|
||
// Show summary
|
||
const details = data.details || {};
|
||
document.getElementById('aiOpsSummarySuccess').textContent = details.success || details.scraped || 0;
|
||
document.getElementById('aiOpsSummaryFailed').textContent = details.failed || 0;
|
||
|
||
if (details.skipped !== undefined) {
|
||
document.getElementById('aiOpsSummarySkippedRow').style.display = 'flex';
|
||
document.getElementById('aiOpsSummarySkipped').textContent = details.skipped;
|
||
}
|
||
|
||
if (details.processing_time) {
|
||
document.getElementById('aiOpsSummaryTime').textContent = `${details.processing_time}s`;
|
||
}
|
||
|
||
document.getElementById('aiOpsSummary').style.display = 'grid';
|
||
document.getElementById('aiOpsActions').style.display = 'flex';
|
||
document.getElementById('aiOpsCloseBtn').style.display = 'block';
|
||
}
|
||
|
||
function startSSEOperation(endpoint, title, icon, limit) {
|
||
openAiOpsModal(title, icon);
|
||
|
||
const url = `${endpoint}?limit=${limit}`;
|
||
aiOpsEventSource = new EventSource(url);
|
||
|
||
aiOpsEventSource.onmessage = function(event) {
|
||
const data = JSON.parse(event.data);
|
||
|
||
if (data.status === 'complete' || data.status === 'error') {
|
||
aiOpsEventSource.close();
|
||
aiOpsEventSource = null;
|
||
completeAiOpsModal(data);
|
||
} else {
|
||
updateAiOpsProgress(data);
|
||
}
|
||
};
|
||
|
||
aiOpsEventSource.onerror = function(event) {
|
||
console.error('SSE error:', event);
|
||
aiOpsEventSource.close();
|
||
aiOpsEventSource = null;
|
||
completeAiOpsModal({ status: 'error', message: 'Błąd połączenia', details: {} });
|
||
};
|
||
}
|
||
|
||
// ===========================================
|
||
// AI Knowledge Base Functions (with SSE)
|
||
// ===========================================
|
||
|
||
async function scrapeContent() {
|
||
const confirmed = await showConfirm(
|
||
'Czy chcesz rozpocząć scrapowanie treści artykułów?<br><br>' +
|
||
'<small>Proces pobierze pełną treść z zatwierdzonych newsów które jeszcze nie mają treści.<br>' +
|
||
'Postęp będzie wyświetlany na żywo.</small>',
|
||
{ icon: '📄', title: 'Scraping treści', okText: 'Rozpocznij', okClass: 'btn-primary' }
|
||
);
|
||
|
||
if (!confirmed) return;
|
||
|
||
startSSEOperation('/admin/zopk/news/scrape-content/stream', 'Scraping treści artykułów', '📄', 30);
|
||
}
|
||
|
||
async function extractKnowledge() {
|
||
const confirmed = await showConfirm(
|
||
'Czy chcesz uruchomić ekstrakcję wiedzy przez AI?<br><br>' +
|
||
'<small>Gemini AI przeanalizuje zescrapowane artykuły i wyekstrahuje:<br>' +
|
||
'• Chunks (fragmenty tekstu)<br>' +
|
||
'• Fakty (daty, liczby, decyzje)<br>' +
|
||
'• Encje (firmy, osoby, projekty)<br><br>' +
|
||
'Postęp będzie wyświetlany na żywo.</small>',
|
||
{ icon: '🤖', title: 'Ekstrakcja wiedzy', okText: 'Uruchom AI', okClass: 'btn-primary' }
|
||
);
|
||
|
||
if (!confirmed) return;
|
||
|
||
startSSEOperation('/admin/zopk/knowledge/extract/stream', 'Ekstrakcja wiedzy (Gemini AI)', '🤖', 10);
|
||
}
|
||
|
||
async function generateEmbeddings() {
|
||
const confirmed = await showConfirm(
|
||
'Czy chcesz wygenerować embeddingi dla semantic search?<br><br>' +
|
||
'<small>Google Text Embedding API przekształci tekst w wektory 768-wymiarowe.<br>' +
|
||
'Embeddingi umożliwiają inteligentne wyszukiwanie w bazie wiedzy.<br><br>' +
|
||
'Postęp będzie wyświetlany na żywo.</small>',
|
||
{ icon: '🔍', title: 'Generowanie embeddingów', okText: 'Generuj', okClass: 'btn-primary' }
|
||
);
|
||
|
||
if (!confirmed) return;
|
||
|
||
startSSEOperation('/admin/zopk/knowledge/embeddings/stream', 'Generowanie embeddingów', '🔍', 50);
|
||
}
|
||
|
||
// Keep old code for backward compatibility (non-SSE version - can be removed later)
|
||
async function scrapeContentOld() {
|
||
const btn = document.getElementById('scrapeBtn');
|
||
const originalContent = btn.innerHTML;
|
||
|
||
btn.disabled = true;
|
||
btn.innerHTML = '<span class="spinner-small"></span>';
|
||
|
||
try {
|
||
const response = await fetch('/admin/zopk/news/scrape-content', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken },
|
||
body: JSON.stringify({ limit: 30 })
|
||
});
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
showToast(data.message || `Zescrapowano ${data.scraped || 0} artykułów`, 'success');
|
||
loadKnowledgeStats();
|
||
} else {
|
||
showToast(data.error || 'Błąd scrapowania', 'error');
|
||
}
|
||
} catch (error) {
|
||
showToast(`Błąd: ${error.message}`, 'error');
|
||
} finally {
|
||
btn.disabled = false;
|
||
btn.innerHTML = originalContent;
|
||
}
|
||
}
|
||
|
||
async function extractKnowledgeOld() {
|
||
const btn = document.getElementById('extractBtn');
|
||
const originalContent = btn.innerHTML;
|
||
|
||
btn.disabled = true;
|
||
btn.innerHTML = '<span class="spinner-small"></span>';
|
||
|
||
try {
|
||
const response = await fetch('/admin/zopk/knowledge/extract', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken },
|
||
body: JSON.stringify({ limit: 10 })
|
||
});
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
showToast(data.message || 'Ekstrakcja zakończona', 'success');
|
||
loadKnowledgeStats();
|
||
} else {
|
||
showToast(data.error || 'Błąd ekstrakcji', 'error');
|
||
}
|
||
} catch (error) {
|
||
showToast(`Błąd: ${error.message}`, 'error');
|
||
} finally {
|
||
btn.disabled = false;
|
||
btn.innerHTML = originalContent;
|
||
}
|
||
}
|
||
|
||
async function generateEmbeddingsOld() {
|
||
const btn = document.getElementById('embeddingsBtn');
|
||
const originalContent = btn.innerHTML;
|
||
|
||
btn.disabled = true;
|
||
btn.innerHTML = '<span class="spinner-small"></span>';
|
||
|
||
try {
|
||
const response = await fetch('/admin/zopk/knowledge/embeddings', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken },
|
||
body: JSON.stringify({ limit: 50 })
|
||
});
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
showToast(data.message || 'Embeddingi wygenerowane', 'success');
|
||
loadKnowledgeStats();
|
||
} else {
|
||
showToast(data.error || 'Błąd generowania', 'error');
|
||
}
|
||
} catch (error) {
|
||
showToast(`Błąd: ${error.message}`, 'error');
|
||
} finally {
|
||
btn.disabled = false;
|
||
btn.innerHTML = originalContent;
|
||
}
|
||
}
|
||
|
||
// Load knowledge stats on page load
|
||
document.addEventListener('DOMContentLoaded', loadKnowledgeStats);
|
||
{% endblock %}
|