From ea8dc3790511b7ce24f58d898ca1f218197ba82e Mon Sep 17 00:00:00 2001 From: kempersc Date: Tue, 13 Jan 2026 20:49:47 +0100 Subject: [PATCH] feat(entity-review): add wrong person detection and confidence filtering - Add is_likely_wrong_person and wrong_person_reason fields to MatchCandidate - Add confidence_original field for tracking pre-adjustment scores - Add visual indicators: AlertTriangle for wrong person, Star for high confidence - Add filter checkboxes: 'Show high confidence (>80%)' and 'Hide wrong person' - Add wrong person alert banner with bilingual labels (NL/EN) - Add danger stat card showing count of likely wrong person matches - Style signal badges by type: danger (birth_year_mismatch), success (validated) - Add extensive CSS for wrong-person/high-confidence alerts and candidate styling --- .../schemas/20251121/linkml/manifest.json | 2 +- frontend/src/pages/EntityReviewPage.css | 298 ++++++++++++++++++ frontend/src/pages/EntityReviewPage.tsx | 99 +++++- 3 files changed, 392 insertions(+), 7 deletions(-) diff --git a/frontend/public/schemas/20251121/linkml/manifest.json b/frontend/public/schemas/20251121/linkml/manifest.json index ca5c32814a..bdb3870f0e 100644 --- a/frontend/public/schemas/20251121/linkml/manifest.json +++ b/frontend/public/schemas/20251121/linkml/manifest.json @@ -1,5 +1,5 @@ { - "generated": "2026-01-13T19:37:31.837Z", + "generated": "2026-01-13T19:48:38.429Z", "schemaRoot": "/schemas/20251121/linkml", "totalFiles": 2894, "categoryCounts": { diff --git a/frontend/src/pages/EntityReviewPage.css b/frontend/src/pages/EntityReviewPage.css index 4e51d4660b..353b751de0 100644 --- a/frontend/src/pages/EntityReviewPage.css +++ b/frontend/src/pages/EntityReviewPage.css @@ -837,6 +837,293 @@ color: #22c55e; } +/* =================================== + Wrong Person & High Confidence Styles + =================================== */ + +/* Wrong Person Alert Banner */ +.wrong-person-alert { + display: flex; + align-items: flex-start; + gap: 0.75rem; + padding: 0.75rem 1rem; + background: linear-gradient(135deg, rgba(239, 68, 68, 0.15), rgba(239, 68, 68, 0.08)); + border: 1px solid rgba(239, 68, 68, 0.3); + border-radius: 8px; + margin: 0.5rem 0.75rem; +} + +.wrong-person-alert .alert-icon { + color: #ef4444; + flex-shrink: 0; + margin-top: 0.125rem; +} + +.wrong-person-alert .alert-content { + flex: 1; +} + +.wrong-person-alert .alert-title { + font-weight: 600; + font-size: 0.875rem; + color: #ef4444; + margin: 0 0 0.25rem; +} + +.wrong-person-alert .alert-message { + font-size: 0.8125rem; + color: var(--text-secondary, #666); + margin: 0; + line-height: 1.4; +} + +.dark .wrong-person-alert .alert-message { + color: var(--text-secondary, #999); +} + +/* High Confidence Alert Banner */ +.high-confidence-alert { + display: flex; + align-items: flex-start; + gap: 0.75rem; + padding: 0.75rem 1rem; + background: linear-gradient(135deg, rgba(34, 197, 94, 0.15), rgba(234, 179, 8, 0.08)); + border: 1px solid rgba(34, 197, 94, 0.3); + border-radius: 8px; + margin: 0.5rem 0.75rem; +} + +.high-confidence-alert .alert-icon { + color: #eab308; + flex-shrink: 0; + margin-top: 0.125rem; +} + +.high-confidence-alert .alert-content { + flex: 1; +} + +.high-confidence-alert .alert-title { + font-weight: 600; + font-size: 0.875rem; + color: #22c55e; + margin: 0 0 0.25rem; +} + +.high-confidence-alert .alert-message { + font-size: 0.8125rem; + color: var(--text-secondary, #666); + margin: 0; + line-height: 1.4; +} + +.dark .high-confidence-alert .alert-message { + color: var(--text-secondary, #999); +} + +/* Candidate Item - Wrong Person Styling */ +.candidate-item.wrong-person { + border-color: rgba(239, 68, 68, 0.5); + background: rgba(239, 68, 68, 0.05); +} + +.candidate-item.wrong-person:hover { + border-color: #ef4444; +} + +.dark .candidate-item.wrong-person { + background: rgba(239, 68, 68, 0.1); +} + +/* Candidate Item - High Confidence Styling */ +.candidate-item.high-confidence { + border-color: rgba(234, 179, 8, 0.5); + background: rgba(234, 179, 8, 0.05); +} + +.candidate-item.high-confidence:hover { + border-color: #eab308; +} + +.dark .candidate-item.high-confidence { + background: rgba(234, 179, 8, 0.1); +} + +/* Combined: High confidence + wrong person */ +.candidate-item.high-confidence.wrong-person { + border-color: rgba(239, 68, 68, 0.5); + background: linear-gradient(135deg, rgba(239, 68, 68, 0.08), rgba(234, 179, 8, 0.05)); +} + +/* Candidate Icons */ +.wrong-person-icon { + color: #ef4444; + flex-shrink: 0; +} + +.high-confidence-icon { + color: #eab308; + flex-shrink: 0; +} + +/* Signal Badge Variants */ +.signal-badge.danger { + background: rgba(239, 68, 68, 0.15); + color: #ef4444; +} + +.signal-badge.success { + background: rgba(34, 197, 94, 0.15); + color: #22c55e; +} + +.signal-badge.warning { + background: rgba(245, 158, 11, 0.15); + color: #f59e0b; +} + +.signal-badge.info { + background: rgba(59, 130, 246, 0.15); + color: #3b82f6; +} + +/* Stat Card - Danger Variant */ +.stat-card.danger .stat-value { + color: #ef4444; +} + +.stat-card.danger { + background: rgba(239, 68, 68, 0.08); + border: 1px solid rgba(239, 68, 68, 0.2); +} + +.stat-card .stat-icon { + display: flex; + align-items: center; + gap: 0.25rem; +} + +.stat-card .stat-icon.danger { + color: #ef4444; +} + +.stat-card .stat-icon.success { + color: #22c55e; +} + +/* Filter Checkbox Styles */ +.filter-checkbox { + display: inline-flex; + align-items: center; + gap: 0.375rem; + padding: 0.375rem 0.625rem; + background: var(--bg-secondary, #f5f5f5); + border: 1px solid var(--border-color, #e0e0e0); + border-radius: 6px; + font-size: 0.75rem; + cursor: pointer; + transition: all 0.15s; + user-select: none; +} + +.filter-checkbox:hover { + border-color: var(--primary-color, #3b82f6); +} + +.filter-checkbox.active { + background: rgba(59, 130, 246, 0.1); + border-color: var(--primary-color, #3b82f6); +} + +.dark .filter-checkbox { + background: var(--bg-tertiary, #2a2a4a); + border-color: var(--border-color, #2a2a4a); +} + +.dark .filter-checkbox:hover { + border-color: var(--primary-color, #3b82f6); +} + +.filter-checkbox input[type="checkbox"] { + margin: 0; + cursor: pointer; +} + +.filter-checkbox .filter-label { + color: var(--text-primary, #1a1a2e); +} + +.dark .filter-checkbox .filter-label { + color: var(--text-primary, #e0e0e0); +} + +/* Filter Icon Colors */ +.filter-icon { + flex-shrink: 0; +} + +.filter-icon.wrong-person { + color: #ef4444; +} + +.filter-icon.high-confidence { + color: #eab308; +} + +/* Wrong Person Warning in Candidate List */ +.wrong-person-warning { + display: flex; + align-items: center; + gap: 0.375rem; + font-size: 0.6875rem; + color: #ef4444; + background: rgba(239, 68, 68, 0.1); + padding: 0.25rem 0.5rem; + border-radius: 4px; + margin-top: 0.25rem; +} + +.wrong-person-warning svg { + flex-shrink: 0; +} + +/* Confidence Display */ +.confidence-display { + display: flex; + align-items: center; + gap: 0.25rem; + font-size: 0.75rem; +} + +.confidence-display.high { + color: #22c55e; + font-weight: 600; +} + +.confidence-display.medium { + color: #f59e0b; +} + +.confidence-display.low { + color: #ef4444; +} + +/* Stats Dashboard - Additional Stat Types */ +.stats-dashboard .stat-card.highlight { + position: relative; + overflow: hidden; +} + +.stats-dashboard .stat-card.highlight::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: linear-gradient(90deg, #eab308, #22c55e); +} + /* Responsive */ @media (max-width: 1024px) { .review-content { @@ -860,4 +1147,15 @@ .stats-dashboard { grid-template-columns: repeat(2, 1fr); } + + .filter-checkbox { + padding: 0.25rem 0.5rem; + font-size: 0.6875rem; + } + + .wrong-person-alert, + .high-confidence-alert { + padding: 0.5rem 0.75rem; + margin: 0.375rem 0.5rem; + } } diff --git a/frontend/src/pages/EntityReviewPage.tsx b/frontend/src/pages/EntityReviewPage.tsx index 1c2b8d4242..4e68d84578 100644 --- a/frontend/src/pages/EntityReviewPage.tsx +++ b/frontend/src/pages/EntityReviewPage.tsx @@ -28,10 +28,12 @@ import { Globe, Link2, AlertCircle, + AlertTriangle, Loader2, RefreshCw, Filter, - Key + Key, + Star } from 'lucide-react'; import './EntityReviewPage.css'; @@ -44,11 +46,15 @@ interface MatchCandidate { linkedin_name: string; linkedin_slug: string | null; confidence_score: number; + confidence_original?: number; match_signals: string[]; reviewed: boolean; review_decision: string | null; reviewed_by: string | null; reviewed_at: string | null; + is_likely_wrong_person?: boolean; + wrong_person_reason?: string | null; + email_birth_year?: number | null; } interface ProfileSummary { @@ -88,6 +94,8 @@ interface ReviewStats { reviewed_candidates: number; pending_candidates: number; review_progress_percent: number; + likely_wrong_person?: number; + confidence_scoring_version?: string; decisions: { match: number; not_match: number; @@ -129,6 +137,13 @@ const TEXT = { savingDecision: { nl: 'Beslissing opslaan...', en: 'Saving decision...' }, decisionSaved: { nl: 'Beslissing opgeslagen', en: 'Decision saved' }, error: { nl: 'Fout', en: 'Error' }, + wrongPerson: { nl: 'Verkeerde Persoon', en: 'Wrong Person' }, + wrongPersonWarning: { nl: 'Waarschijnlijk verkeerde persoon!', en: 'Likely wrong person!' }, + likelyWrongPerson: { nl: 'Waarschijnlijk verkeerd', en: 'Likely Wrong' }, + highConfidence: { nl: 'Hoog vertrouwen', en: 'High Confidence' }, + showHighConfidence: { nl: 'Toon hoog vertrouwen (>80%)', en: 'Show high confidence (>80%)' }, + hideWrongPerson: { nl: 'Verberg verkeerde persoon', en: 'Hide wrong person' }, + birthYearMismatch: { nl: 'Geboortejaar komt niet overeen', en: 'Birth year mismatch' }, }; export default function EntityReviewPage() { @@ -167,6 +182,8 @@ export default function EntityReviewPage() { const [showFilters, setShowFilters] = useState(false); const [minSignals, setMinSignals] = useState(1); const [requiredSignals, setRequiredSignals] = useState([]); + const [showHighConfidenceOnly, setShowHighConfidenceOnly] = useState(false); + const [hideWrongPerson, setHideWrongPerson] = useState(false); const [availableSignals, setAvailableSignals] = useState<{signal_types: string[], signal_counts: Record}>({signal_types: [], signal_counts: {}}); // Fetch available signal types @@ -191,6 +208,9 @@ export default function EntityReviewPage() { if (requiredSignals.length > 0) { url += `&signal_types=${requiredSignals.join(',')}`; } + if (showHighConfidenceOnly) { + url += `&min_confidence=0.8`; + } const response = await fetch(url); if (!response.ok) throw new Error('Failed to fetch profiles'); const data = await response.json(); @@ -201,7 +221,7 @@ export default function EntityReviewPage() { } finally { setLoading(false); } - }, [page, minSignals, requiredSignals]); + }, [page, minSignals, requiredSignals, showHighConfidenceOnly]); // Fetch stats const fetchStats = useCallback(async () => { @@ -396,6 +416,13 @@ export default function EntityReviewPage() { {stats.pending_candidates.toLocaleString()} In afwachting + {stats.likely_wrong_person !== undefined && stats.likely_wrong_person > 0 && ( +
+ + {stats.likely_wrong_person.toLocaleString()} + {t('likelyWrongPerson')} +
+ )}
{stats.review_progress_percent}%
@@ -416,6 +443,28 @@ export default function EntityReviewPage() {
+
+ +
+
+ +
@@ -568,20 +617,34 @@ export default function EntityReviewPage() {

{t('matchCandidate')}s ({selectedProfile.match_candidates.length})

    - {selectedProfile.match_candidates.map((candidate) => ( + {selectedProfile.match_candidates + .filter(c => !hideWrongPerson || !c.is_likely_wrong_person) + .map((candidate) => (
  • = 0.8 ? 'high-confidence' : ''}`} onClick={() => { setSelectedCandidate(candidate); fetchLinkedinProfile(candidate.linkedin_ppid); }} >
    - + {candidate.is_likely_wrong_person ? ( + + ) : candidate.confidence_score >= 0.8 ? ( + + ) : ( + + )} {candidate.linkedin_name} {formatConfidence(candidate.confidence_score)}
    + {candidate.is_likely_wrong_person && ( +
    + + {t('wrongPersonWarning')} +
    + )} {candidate.reviewed && ( {candidate.review_decision} @@ -610,6 +673,30 @@ export default function EntityReviewPage() {
) : linkedinProfile ? (
+ {/* Wrong Person Alert Banner */} + {selectedCandidate.is_likely_wrong_person && ( +
+ +
+ {t('wrongPersonWarning')} + {selectedCandidate.wrong_person_reason && ( +

{selectedCandidate.wrong_person_reason}

+ )} +
+
+ )} + + {/* High Confidence Banner */} + {selectedCandidate.confidence_score >= 0.8 && !selectedCandidate.is_likely_wrong_person && ( +
+ +
+ {t('highConfidence')} +

Score: {Math.round(selectedCandidate.confidence_score * 100)}%

+
+
+ )} +
Name {linkedinProfile.name} @@ -634,7 +721,7 @@ export default function EntityReviewPage() { {t('matchSignals')}
{selectedCandidate.match_signals.map((signal, i) => ( - {signal} + {signal} ))}