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:
parent
61b8a8e797
commit
0cbdcaaad6
7
app.py
7
app.py
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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]:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user