feat(entity-review): add 'provides match' toggle for source URLs
All checks were successful
Deploy Frontend / build-and-deploy (push) Successful in 2m23s
DSPy RAG Evaluation / Layer 1 - Unit Tests (push) Successful in 5m37s
DSPy RAG Evaluation / Layer 2 - DSPy Module Tests (push) Successful in 7m24s
DSPy RAG Evaluation / Layer 3 - Integration Tests (push) Successful in 5m47s
DSPy RAG Evaluation / Layer 4 - Comprehensive Evaluation (push) Successful in 6m52s
DSPy RAG Evaluation / Quality Gate (push) Successful in 1s

- Add toggle in source URL form to indicate when a source provides
  sufficient information to create a person profile without LinkedIn
- Store provides_match boolean in source observation data
- Display green badge on existing sources that have provides_match: true
- Include bilingual tooltip (EN/NL) explaining the toggle purpose
This commit is contained in:
kempersc 2026-01-18 18:25:45 +01:00
parent b11223277c
commit 6812524ae5
3 changed files with 116 additions and 11 deletions

View file

@ -2604,6 +2604,67 @@
color: #6b7280; color: #6b7280;
} }
/* Source URL Header (link + badge) */
.source-url-header {
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
}
/* Provides Match Toggle */
.provides-match-toggle {
display: flex;
align-items: center;
padding: 0.5rem 0;
}
.provides-match-toggle .toggle-label {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
font-size: 0.8125rem;
color: var(--text-secondary, #64748b);
}
.provides-match-toggle input[type="checkbox"] {
width: 16px;
height: 16px;
accent-color: #10b981;
cursor: pointer;
}
.provides-match-toggle .toggle-text {
color: var(--text-primary, #1e293b);
}
.dark .provides-match-toggle .toggle-text {
color: var(--text-primary, #e2e8f0);
}
.provides-match-toggle svg {
color: var(--text-secondary, #94a3b8);
cursor: help;
}
/* Provides Match Badge on existing source URLs */
.provides-match-badge {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 2px 6px;
background: rgba(16, 185, 129, 0.15);
color: #10b981;
border-radius: 4px;
font-size: 0.6875rem;
font-weight: 500;
}
.dark .provides-match-badge {
background: rgba(16, 185, 129, 0.2);
}
/* WCMS-Only Profiles Styles */ /* WCMS-Only Profiles Styles */
.wcms-only-search { .wcms-only-search {
position: relative; position: relative;

View file

@ -145,6 +145,7 @@ interface SourceUrlItem {
source_type?: string; source_type?: string;
source_domain?: string; source_domain?: string;
comment?: string; comment?: string;
provides_match?: boolean;
added_at?: string; added_at?: string;
added_manually?: boolean; added_manually?: boolean;
} }
@ -402,6 +403,7 @@ export default function EntityReviewPage() {
const [showSourceUrlInput, setShowSourceUrlInput] = useState(false); const [showSourceUrlInput, setShowSourceUrlInput] = useState(false);
const [sourceUrl, setSourceUrl] = useState(''); const [sourceUrl, setSourceUrl] = useState('');
const [sourceComment, setSourceComment] = useState(''); const [sourceComment, setSourceComment] = useState('');
const [sourceProvidesMatch, setSourceProvidesMatch] = useState(false);
const [addingSourceUrl, setAddingSourceUrl] = useState(false); const [addingSourceUrl, setAddingSourceUrl] = useState(false);
const [sourceUrlError, setSourceUrlError] = useState<string | null>(null); const [sourceUrlError, setSourceUrlError] = useState<string | null>(null);
const [sourceUrlSuccess, setSourceUrlSuccess] = useState<string | null>(null); const [sourceUrlSuccess, setSourceUrlSuccess] = useState<string | null>(null);
@ -714,6 +716,7 @@ export default function EntityReviewPage() {
source_url: sourceUrl.trim(), source_url: sourceUrl.trim(),
comment: sourceComment.trim() || undefined, comment: sourceComment.trim() || undefined,
source_type: 'webpage', source_type: 'webpage',
provides_match: sourceProvidesMatch,
}; };
// For WCMS-only profiles, include WCMS metadata // For WCMS-only profiles, include WCMS metadata
@ -747,6 +750,7 @@ export default function EntityReviewPage() {
// Clear inputs and show success // Clear inputs and show success
setSourceUrl(''); setSourceUrl('');
setSourceComment(''); setSourceComment('');
setSourceProvidesMatch(false);
setSourceUrlSuccess(result.message || (language === 'nl' ? 'Bron toegevoegd' : 'Source added')); setSourceUrlSuccess(result.message || (language === 'nl' ? 'Bron toegevoegd' : 'Source added'));
// Hide success message after 3 seconds // Hide success message after 3 seconds
@ -765,7 +769,7 @@ export default function EntityReviewPage() {
} finally { } finally {
setAddingSourceUrl(false); setAddingSourceUrl(false);
} }
}, [selectedProfile, sourceUrl, sourceComment, language, fetchProfileDetail, fetchWcmsOnlyProfileDetail]); }, [selectedProfile, sourceUrl, sourceComment, sourceProvidesMatch, language, fetchProfileDetail, fetchWcmsOnlyProfileDetail]);
// Save review decision // Save review decision
const saveDecision = useCallback(async (decision: 'match' | 'not_match' | 'uncertain') => { const saveDecision = useCallback(async (decision: 'match' | 'not_match' | 'uncertain') => {
@ -2143,16 +2147,27 @@ export default function EntityReviewPage() {
} }
return ( return (
<div key={source.source_id} className="source-url-item"> <div key={source.source_id} className="source-url-item">
<a <div className="source-url-header">
href={source.source_url} <a
target="_blank" href={source.source_url}
rel="noopener noreferrer" target="_blank"
className="source-url-link" rel="noopener noreferrer"
title={source.source_url} className="source-url-link"
> title={source.source_url}
<Globe size={12} /> >
{displayUrl} <Globe size={12} />
</a> {displayUrl}
</a>
{source.provides_match && (
<span
className="provides-match-badge"
title={language === 'nl' ? 'Bron volstaat als match' : 'Provides match'}
>
<CheckCircle size={10} />
{language === 'nl' ? 'Match' : 'Match'}
</span>
)}
</div>
{source.comment && ( {source.comment && (
<div className="source-url-comment"> <div className="source-url-comment">
"{source.comment}" "{source.comment}"
@ -2204,6 +2219,32 @@ export default function EntityReviewPage() {
disabled={addingSourceUrl} disabled={addingSourceUrl}
rows={2} rows={2}
/> />
<div className="provides-match-toggle">
<label className="toggle-label">
<input
type="checkbox"
checked={sourceProvidesMatch}
onChange={(e) => setSourceProvidesMatch(e.target.checked)}
disabled={addingSourceUrl}
/>
<span className="toggle-text">
{language === 'nl' ? 'Bron volstaat als match' : 'Provides match'}
</span>
<Tooltip
position="top"
maxWidth={300}
content={
<div>
{language === 'nl'
? 'Wanneer ingeschakeld, biedt deze bron voldoende informatie om deze persoon te identificeren en een volledig persoonsprofiel aan te maken zonder LinkedIn-match.'
: 'When enabled, this source provides sufficient information to identify this person and create a complete person profile without needing a LinkedIn match.'}
</div>
}
>
<Info size={14} />
</Tooltip>
</label>
</div>
<button <button
className="add-source-url-btn" className="add-source-url-btn"
onClick={addSourceUrl} onClick={addSourceUrl}

View file

@ -424,6 +424,7 @@ class AddSourceUrlRequest(BaseModel):
source_url: str source_url: str
comment: Optional[str] = None # User comment explaining the source, e.g., "De lessen worden gegeven door Mala Sardjoepersad" comment: Optional[str] = None # User comment explaining the source, e.g., "De lessen worden gegeven door Mala Sardjoepersad"
source_type: Optional[str] = None # Optional type: "webpage", "social_media", "news_article", etc. source_type: Optional[str] = None # Optional type: "webpage", "social_media", "news_article", etc.
provides_match: bool = False # When True, this source provides sufficient info to identify the person without LinkedIn
# Optional WCMS data for profiles that aren't in the candidates file # Optional WCMS data for profiles that aren't in the candidates file
wcms_name: Optional[str] = None wcms_name: Optional[str] = None
wcms_email: Optional[str] = None wcms_email: Optional[str] = None
@ -2634,6 +2635,7 @@ async def add_source_url(request: AddSourceUrlRequest):
wcms_ppid = request.wcms_ppid wcms_ppid = request.wcms_ppid
comment = request.comment.strip() if request.comment else None comment = request.comment.strip() if request.comment else None
source_type = request.source_type or "webpage" source_type = request.source_type or "webpage"
provides_match = request.provides_match
# Load candidates from the aggregated file # Load candidates from the aggregated file
load_candidates() load_candidates()
@ -2681,6 +2683,7 @@ async def add_source_url(request: AddSourceUrlRequest):
"source_type": source_type, "source_type": source_type,
"source_domain": parsed.netloc, "source_domain": parsed.netloc,
"comment": comment, "comment": comment,
"provides_match": provides_match,
"added_at": datetime.now(timezone.utc).isoformat(), "added_at": datetime.now(timezone.utc).isoformat(),
"added_manually": True "added_manually": True
} }