{% extends "base.html" %} {% block title %}ZOP Kaszubia - Panel Admina - Norda Biznes Partner{% endblock %} {% block extra_css %} {% endblock %} {% block content %}

Zielony Okręg Przemysłowy Kaszubia

Panel zarządzania bazą wiedzy

Zobacz stronę publiczną Zarządzaj newsami

Baza wiedzy ZOP Kaszubia

{{ stats.total_projects }}
Projektów
{{ stats.total_stakeholders }}
Interesariuszy
{{ stats.total_resources }}
Materiałów

Filtruj newsy (kliknij aby wybrać)

{{ stats.pending_news }}
Oczekujących
{{ stats.approved_news }}
Zatwierdzonych
{{ stats.rejected_news }}
Odrzuconych

Ocena AI (Gemini) (kliknij aby wybrać)

🤖 Model: gemini-2.5-flash-lite
📅 Prompt v2: 2026-01-15
✅ Nowe tematy: +7 projektów infrastrukturalnych
{{ stats.ai_relevant }}
Pasuje wg AI
{{ stats.ai_not_relevant }}
Nie pasuje wg AI
{{ stats.ai_not_evaluated }}
Nieocenione
{% if stats.ai_missing_score > 0 %}
{{ stats.ai_missing_score }}
Brak gwiazdek
{% endif %}
{% if stats.old_news > 0 and not show_old %}
⚠️ {{ stats.old_news }} newsów z przed 2024 roku zostało ukrytych (ZOP Kaszubia powstał w 2024). Pokaż wszystkie lub odrzuć wszystkie stare.
{% endif %}

Wyszukaj nowe artykuły

Multi-source search: Brave API, RSS (trojmiasto.pl, Dziennik Bałtycki), Google News, gov.pl

Inicjalizacja... 0%

{% 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 }})

{% if news_items %}
{% for news in news_items %}

{{ news.title }}

{% if news.source_type %} {% 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 %} {% endif %} {{ news.source_name or news.source_domain or '-' }} {% set news_year = news.published_at.year if news.published_at else None %} {{ news.published_at.strftime('%d.%m.%Y') if news.published_at else (news.created_at.strftime('%d.%m.%Y') if news.created_at else '-') }} {% if news_year and news_year < min_year %} ⚠️ Sprzed {{ min_year }} {% endif %} {% if news.confidence_score %} {% if news.source_count and news.source_count > 1 %}{{ news.source_count }} źródeł{% else %}1 źródło{% endif %} {% endif %} {% if news.status == 'approved' or news.status == 'auto_approved' %} ✓ {{ 'Auto' if news.status == 'auto_approved' else '' }}Zatwierdzony {% elif news.status == 'rejected' %} ✗ Odrzucony {% endif %} {# AI Evaluation badge with star rating #} {% if news.ai_relevant is not none %} 🤖 {% for i in range(1, 6) %} {% endfor %} {% endif %}
{% if news.status == 'pending' %} {% elif news.status == 'rejected' %} {% endif %}
{% endfor %}
{% if total_pages > 1 %} {% endif %} {% else %}

Brak newsów{% if status_filter != 'all' %} o tym statusie{% endif %}.

{% endif %}

🧠 Baza Wiedzy AI

Ładowanie statystyk...

Projekty strategiczne

{% if projects %} {% for project in projects %} {% endfor %}
Nazwa Typ Status Region
{{ project.name }}
{{ project.slug }}
{{ project.project_type or '-' }} {% if project.status == 'planned' %}Planowany{% elif project.status == 'in_progress' %}W realizacji{% else %}{{ project.status }}{% endif %} {{ project.region or '-' }}
{% else %}

Brak projektów.

{% endif %}
{% if fetch_jobs %}

Ostatnie wyszukiwania

{% for job in fetch_jobs %}
{{ job.search_query }}
{{ job.created_at.strftime('%d.%m.%Y %H:%M') if job.created_at else '-' }}
Znaleziono: {{ job.results_found or 0 }} | Nowych: {{ job.results_new or 0 }}
{{ job.status }}
{% endfor %}
{% endif %}
{% 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 = ` ${icons[type] || icons.info} ${message} `; 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 ${minYear} roku?

` + `ZOP Kaszubia powstał w 2024 roku, więc starsze artykuły najprawdopodobniej nie dotyczą tego projektu.`, { 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 = `
${data.relevant_count}
Pasuje do ZOP Kaszubia
${data.not_relevant_count}
Nie pasuje
${data.total_evaluated}
Ocenionych

Odświeżam stronę za 2 sekundy...

`; // 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 = `

${data.error}

`; 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 = `

Przetwarzanie trwało zbyt długo (>3 min).
Spróbuj ponownie lub sprawdź logi serwera.

`; } else { resultTitle.textContent = 'Błąd połączenia'; resultStats.innerHTML = `

${error.message}

`; } 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 = `
${data.total_evaluated}
Ocenionych
${data.relevant_count}
Pasuje
${data.not_relevant_count}
Nie pasuje

${data.message}

`; setTimeout(() => location.reload(), 2000); } else { resultIcon.textContent = '✗'; resultIcon.className = 'modal-icon error'; resultTitle.textContent = 'Wystąpił błąd'; resultStats.innerHTML = `

${data.error}

`; 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 = `

${error.message}

`; 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 = `
${data.details.map(d => `
${d.old_score}★ → ${d.new_score}★ ${d.title}...
`).join('')}
`; } resultStats.innerHTML = `
${data.total_evaluated}
Ocenionych
${data.upgraded}
⬆️ Podwyższono
${data.downgraded}
⬇️ Obniżono
${data.unchanged}
Bez zmian

${data.message}

${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 = `

${data.error}

`; 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 = `

${error.message}

`; 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 => `
${phase.icon} ${phase.label}
`).join(''); // Build initial progress steps (will be populated from process_log) progressSteps.innerHTML = '
Inicjalizacja...
'; // 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 => `
${log.message} ${log.count > 0 ? `${log.count}` : ''}
`).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 = `
${data.total_found || 0}
Znaleziono
${(data.blacklisted || 0) + (data.keyword_filtered || 0)}
Odfiltrowano
${data.ai_rejected || 0}
AI odrzucił
${data.ai_approved || 0}
AI zaakceptował
${data.saved_new || 0}
Nowe w bazie
`; // 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 `
${stars} ${article.title} ${article.source || ''}
`; }).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 => `
${stat.icon} ${stat.label} ${stat.value}
`).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 `
${stars} ${article.title} ${article.source || ''}
`; }).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 = `
Błąd: ${data.error}
`; 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 = `
Błąd połączenia: ${error.message}
`; 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 => `${e.name} (${e.mentions})` ).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 = ` ${time} ${message} `; 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?

' + 'Proces pobierze pełną treść z zatwierdzonych newsów które jeszcze nie mają treści.
' + 'Postęp będzie wyświetlany na żywo.
', { 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?

' + 'Gemini AI przeanalizuje zescrapowane artykuły i wyekstrahuje:
' + '• Chunks (fragmenty tekstu)
' + '• Fakty (daty, liczby, decyzje)
' + '• Encje (firmy, osoby, projekty)

' + 'Postęp będzie wyświetlany na żywo.
', { 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?

' + 'Google Text Embedding API przekształci tekst w wektory 768-wymiarowe.
' + 'Embeddingi umożliwiają inteligentne wyszukiwanie w bazie wiedzy.

' + 'Postęp będzie wyświetlany na żywo.
', { 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 = ''; 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 = ''; 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 = ''; 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 %}