feat(zopk): Szczegółowe statystyki wyników wyszukiwania newsów

- Zamiana auto-odświeżania na przycisk OK
- Dodanie sekcji szczegółowych statystyk (12 metryk)
- Dodanie listy artykułów odrzuconych przez AI
- Śledzenie czasu przetwarzania
- API zwraca nowe pola: sent_to_ai, ai_rejected_articles, processing_time

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-01-15 05:28:10 +01:00
parent 61b8a8e797
commit 0cbdcaaad6
3 changed files with 113 additions and 17 deletions

7
app.py
View File

@ -10744,9 +10744,14 @@ def api_zopk_search_news():
'ai_rejected': results.get('ai_rejected', 0),
'blacklisted': results.get('blacklisted', 0),
'keyword_filtered': results.get('keyword_filtered', 0),
'sent_to_ai': results.get('sent_to_ai', 0),
'duplicates': results.get('duplicates', 0),
'processing_time': results.get('processing_time', 0),
'knowledge_entities_created': results.get('knowledge_entities_created', 0),
'source_stats': results['source_stats'],
'process_log': results.get('process_log', []),
'auto_approved_articles': results.get('auto_approved_articles', [])
'auto_approved_articles': results.get('auto_approved_articles', []),
'ai_rejected_articles': results.get('ai_rejected_articles', [])
})
except Exception as e:

View File

@ -1120,10 +1120,28 @@
<div class="auto-approved-list" id="autoApprovedList"></div>
</div>
<!-- Countdown to refresh -->
<div class="refresh-countdown" id="refreshCountdown">
<span>Odświeżam za <strong id="countdownSeconds">8</strong> sekund...</span>
<button type="button" class="btn btn-sm btn-secondary" onclick="location.reload()">Odśwież teraz</button>
<!-- 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>
@ -2101,17 +2119,72 @@ async function searchNews() {
}).join('');
}
// Start countdown to refresh (8 seconds)
let countdown = 8;
const countdownEl = document.getElementById('countdownSeconds');
const countdownInterval = setInterval(() => {
countdown--;
countdownEl.textContent = countdown;
if (countdown <= 0) {
clearInterval(countdownInterval);
location.reload();
}
}, 1000);
// 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

View File

@ -633,6 +633,9 @@ class ZOPKNewsService:
Returns:
Dict with search results, statistics, and detailed process log
"""
import time
start_time = time.time()
all_items: List[NewsItem] = []
source_stats = {
'brave_queries': 0,
@ -647,6 +650,7 @@ class ZOPKNewsService:
# Process log for frontend progress display
process_log = []
auto_approved_articles = [] # Track articles auto-approved (3+★)
ai_rejected_articles = [] # Track articles rejected by AI (1-2★)
# 1. BRAVE SEARCH - Multiple precise queries
process_log.append({
@ -796,6 +800,8 @@ class ZOPKNewsService:
})
# 6. AI EVALUATION (before saving) - only if enabled
sent_to_ai = len(verified_items) # Track before AI modifies the list
if self.enable_ai_prefilter and self._get_gemini():
process_log.append({
'phase': 'ai',
@ -842,6 +848,11 @@ class ZOPKNewsService:
else:
# Low score - reject before saving
source_stats['ai_rejected'] += 1
ai_rejected_articles.append({
'title': item['title'][:80] + ('...' if len(item['title']) > 80 else ''),
'score': ai_score,
'source': item.get('source_name', item.get('source_domain', ''))
})
logger.debug(f"AI rejected ({ai_score}★): {item['title'][:50]}")
else:
# AI evaluation failed - save as pending for manual review
@ -897,19 +908,26 @@ class ZOPKNewsService:
'count': saved_count
})
processing_time = time.time() - start_time
return {
'total_found': source_stats['brave_results'] + source_stats['rss_results'],
'blacklisted': source_stats['blacklisted'],
'keyword_filtered': source_stats['keyword_filtered'],
'sent_to_ai': sent_to_ai,
'ai_rejected': source_stats['ai_rejected'],
'ai_approved': source_stats['ai_approved'],
'unique_items': len(verified_items),
'saved_new': saved_count,
'updated_existing': updated_count,
'duplicates': updated_count, # Updated = duplicates that existed
'source_stats': source_stats,
'auto_approved': auto_approved_count,
'process_log': process_log,
'auto_approved_articles': auto_approved_articles
'auto_approved_articles': auto_approved_articles,
'ai_rejected_articles': ai_rejected_articles,
'processing_time': processing_time,
'knowledge_entities_created': saved_count # Same as saved_new for now
}
def _search_brave_single(self, query: str) -> List[NewsItem]: