feat: Add logo recommendation scoring to candidate gallery
Some checks are pending
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run

Score candidates based on format (SVG > raster), source reliability
(img_scan > apple-touch-icon > og:image > favicon), aspect ratio
(square-ish preferred), and size. Recommended candidate gets amber
border with "rekomendowane" badge in gallery UI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-02-18 17:29:31 +01:00
parent 4119e44a58
commit 788ac12d30
2 changed files with 65 additions and 3 deletions

View File

@ -112,6 +112,12 @@ class LogoFetchService:
if not saved:
return {'success': False, 'message': 'Nie udało się pobrać żadnego kandydata', 'candidates': [], 'steps': steps}
# Score candidates and find recommendation
recommended_index = None
if saved:
best = max(saved, key=lambda c: self._score_candidate(c))
recommended_index = best['index']
steps.append({
'step': 'save',
'status': 'complete',
@ -122,6 +128,7 @@ class LogoFetchService:
'success': True,
'message': f'Znaleziono {len(saved)} kandydatów na logo',
'candidates': saved,
'recommended_index': recommended_index,
'steps': steps
}
@ -408,6 +415,53 @@ class LogoFetchService:
else:
steps.append({'step': 'scan_images', 'status': 'missing', 'message': 'Brak elementów img z "logo" w atrybutach'})
@staticmethod
def _score_candidate(candidate):
"""Score a candidate for recommendation. Higher = better logo choice."""
score = 0
# SVG is ideal for logos (vector, scalable)
if candidate['ext'] == 'svg':
score += 30
# Source reliability — how likely this is the actual logo
source_scores = {
'img_scan': 35,
'css_bg': 25,
'apple-touch-icon': 15,
'og:image': 0,
'twitter:image': 0,
'favicon': -10,
'google_favicon': -20,
}
score += source_scores.get(candidate['source'], 0)
# Aspect ratio — square-ish logos are most versatile
w, h = candidate.get('width', 0), candidate.get('height', 0)
if w > 0 and h > 0:
ratio = max(w, h) / min(w, h)
if ratio <= 2.0:
score += 20
elif ratio <= 3.0:
score += 5
else:
score -= 15
elif candidate['ext'] == 'svg':
score += 15
# Size — prefer medium-sized, not favicon-tiny or banner-huge
max_dim = max(w, h) if w > 0 else 0
if max_dim == 0 and candidate['ext'] == 'svg':
score += 10
elif 64 <= max_dim <= 512:
score += 15
elif max_dim > 512:
score += 5
elif max_dim > 0:
score -= 10
return score
@staticmethod
def _parse_size(sizes_str):
"""Parse '180x180' to max dimension int."""

View File

@ -446,6 +446,11 @@
background: #d1fae5;
color: #065f46;
}
.logo-gallery-item.recommended { border-color: #f59e0b; }
.logo-gallery-item.recommended .gallery-badge.badge-recommended {
background: #fef3c7;
color: #92400e;
}
.logo-gallery-item .gallery-size {
font-size: 10px;
color: #9ca3af;
@ -4707,7 +4712,7 @@ function updateLogoStep(stepId, status, message) {
return resp.json();
}
function buildGallery(candidates, existingLogoExt) {
function buildGallery(candidates, existingLogoExt, recommendedIndex) {
const grid = document.getElementById('logoGalleryGrid');
grid.innerHTML = '';
selectedIndex = null;
@ -4731,15 +4736,18 @@ function updateLogoStep(stepId, status, message) {
// Show candidates
candidates.forEach(c => {
const item = document.createElement('div');
item.className = 'logo-gallery-item';
const isRecommended = (recommendedIndex !== null && c.index === recommendedIndex);
item.className = 'logo-gallery-item' + (isRecommended ? ' recommended' : '');
item.dataset.index = c.index;
const dims = c.width ? `${c.width}x${c.height}` : 'SVG';
const recBadge = isRecommended ? '<span class="gallery-badge badge-recommended">rekomendowane</span>' : '';
item.innerHTML = `
<div class="gallery-img">
<img src="/static/img/companies/${c.filename}${cacheBust}" alt="${c.label}">
</div>
<div class="gallery-label">${c.label}</div>
<div class="gallery-size">${dims}</div>
${recBadge}
`;
item.addEventListener('click', () => selectCandidate(item, c.index));
grid.appendChild(item);
@ -4787,7 +4795,7 @@ function updateLogoStep(stepId, status, message) {
}
// Show gallery with all candidates
buildGallery(data.candidates, data.existing_logo_ext);
buildGallery(data.candidates, data.existing_logo_ext, data.recommended_index);
document.getElementById('logoGalleryOverlay').classList.add('active');
} catch (error) {