Remove deprecated slot definitions and add archived versions for future reference
- Deleted the following slot definitions: - wikidata_class_slot - wikidata_entity_label_slot - wikidata_mapping_rationale_slot - word_count_slot - Added archived versions of the deleted slots to preserve historical data: - wikidata_class_archived_20260114.yaml - wikidata_entity_label_archived_20260114.yaml - wikidata_mapping_rationale_archived_20260114.yaml - word_count_archived_20260114.yaml - Introduced a new hook `usePersonSearch` for enhanced semantic search functionality in the frontend, supporting debounced queries and caching.
This commit is contained in:
parent
1389b744f1
commit
d5d970b513
13 changed files with 4332 additions and 3245 deletions
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"generated": "2026-01-14T21:33:36.352Z",
|
||||
"generated": "2026-01-14T21:38:51.740Z",
|
||||
"schemaRoot": "/schemas/20251121/linkml",
|
||||
"totalFiles": 3026,
|
||||
"categoryCounts": {
|
||||
|
|
|
|||
238
frontend/src/hooks/usePersonSearch.ts
Normal file
238
frontend/src/hooks/usePersonSearch.ts
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
/**
|
||||
* usePersonSearch Hook
|
||||
*
|
||||
* Provides semantic search functionality for person profiles using
|
||||
* the RAG API's Qdrant vector database backend.
|
||||
*
|
||||
* Features:
|
||||
* - Semantic vector search across all profiles
|
||||
* - Filter by field type (name, email, domain, birth_year)
|
||||
* - Debounced queries to prevent API overload
|
||||
* - Caching of search results
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
|
||||
export type SearchField = 'all' | 'name' | 'email' | 'domain' | 'birth_year';
|
||||
|
||||
export interface PersonSearchResult {
|
||||
ppid?: string;
|
||||
name: string;
|
||||
headline?: string | null;
|
||||
custodian_name?: string | null;
|
||||
custodian_slug?: string | null;
|
||||
linkedin_url?: string | null;
|
||||
heritage_relevant?: boolean | null;
|
||||
heritage_type?: string | null;
|
||||
location?: string | null;
|
||||
email?: string | null;
|
||||
email_domain?: string | null;
|
||||
birth_year?: number | null;
|
||||
score?: number | null;
|
||||
}
|
||||
|
||||
export interface PersonSearchResponse {
|
||||
query: string;
|
||||
results: PersonSearchResult[];
|
||||
result_count: number;
|
||||
query_time_ms: number;
|
||||
collection_stats?: Record<string, unknown> | null;
|
||||
embedding_model_used?: string | null;
|
||||
}
|
||||
|
||||
interface UsePersonSearchOptions {
|
||||
debounceMs?: number;
|
||||
minQueryLength?: number;
|
||||
maxResults?: number;
|
||||
}
|
||||
|
||||
interface UsePersonSearchReturn {
|
||||
// Search state
|
||||
query: string;
|
||||
setQuery: (query: string) => void;
|
||||
searchField: SearchField;
|
||||
setSearchField: (field: SearchField) => void;
|
||||
|
||||
// Results
|
||||
results: PersonSearchResult[];
|
||||
isSearching: boolean;
|
||||
error: string | null;
|
||||
|
||||
// Metadata
|
||||
queryTimeMs: number | null;
|
||||
resultCount: number;
|
||||
embeddingModelUsed: string | null;
|
||||
|
||||
// Actions
|
||||
clearSearch: () => void;
|
||||
search: (query: string, field?: SearchField) => Promise<void>;
|
||||
}
|
||||
|
||||
const API_BASE = '/api/rag';
|
||||
|
||||
export function usePersonSearch(options: UsePersonSearchOptions = {}): UsePersonSearchReturn {
|
||||
const {
|
||||
debounceMs = 300,
|
||||
minQueryLength = 2,
|
||||
maxResults = 50,
|
||||
} = options;
|
||||
|
||||
// State
|
||||
const [query, setQuery] = useState('');
|
||||
const [searchField, setSearchField] = useState<SearchField>('all');
|
||||
const [results, setResults] = useState<PersonSearchResult[]>([]);
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [queryTimeMs, setQueryTimeMs] = useState<number | null>(null);
|
||||
const [resultCount, setResultCount] = useState(0);
|
||||
const [embeddingModelUsed, setEmbeddingModelUsed] = useState<string | null>(null);
|
||||
|
||||
// Refs for debouncing
|
||||
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
|
||||
// Build the search query based on field selection
|
||||
const buildSearchQuery = useCallback((rawQuery: string, field: SearchField): string => {
|
||||
const trimmedQuery = rawQuery.trim();
|
||||
if (!trimmedQuery) return '';
|
||||
|
||||
switch (field) {
|
||||
case 'name':
|
||||
return `person named ${trimmedQuery}`;
|
||||
case 'email':
|
||||
return `email address ${trimmedQuery}`;
|
||||
case 'domain':
|
||||
return `working at domain ${trimmedQuery}`;
|
||||
case 'birth_year':
|
||||
return `born in ${trimmedQuery}`;
|
||||
case 'all':
|
||||
default:
|
||||
return trimmedQuery;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Perform the actual search
|
||||
const performSearch = useCallback(async (searchQuery: string, field: SearchField) => {
|
||||
if (searchQuery.trim().length < minQueryLength) {
|
||||
setResults([]);
|
||||
setResultCount(0);
|
||||
setQueryTimeMs(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Cancel any pending request
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
}
|
||||
abortControllerRef.current = new AbortController();
|
||||
|
||||
setIsSearching(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const enhancedQuery = buildSearchQuery(searchQuery, field);
|
||||
|
||||
const response = await fetch(`${API_BASE}/persons/search`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: enhancedQuery,
|
||||
k: maxResults,
|
||||
only_heritage_relevant: false,
|
||||
}),
|
||||
signal: abortControllerRef.current.signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Search failed: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data: PersonSearchResponse = await response.json();
|
||||
|
||||
setResults(data.results);
|
||||
setResultCount(data.result_count);
|
||||
setQueryTimeMs(data.query_time_ms);
|
||||
setEmbeddingModelUsed(data.embedding_model_used || null);
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.name === 'AbortError') {
|
||||
// Request was cancelled, ignore
|
||||
return;
|
||||
}
|
||||
console.error('Person search error:', err);
|
||||
setError(err instanceof Error ? err.message : 'Search failed');
|
||||
setResults([]);
|
||||
} finally {
|
||||
setIsSearching(false);
|
||||
}
|
||||
}, [buildSearchQuery, maxResults, minQueryLength]);
|
||||
|
||||
// Debounced search effect
|
||||
useEffect(() => {
|
||||
if (debounceTimerRef.current) {
|
||||
clearTimeout(debounceTimerRef.current);
|
||||
}
|
||||
|
||||
if (!query.trim()) {
|
||||
setResults([]);
|
||||
setResultCount(0);
|
||||
setQueryTimeMs(null);
|
||||
return;
|
||||
}
|
||||
|
||||
debounceTimerRef.current = setTimeout(() => {
|
||||
performSearch(query, searchField);
|
||||
}, debounceMs);
|
||||
|
||||
return () => {
|
||||
if (debounceTimerRef.current) {
|
||||
clearTimeout(debounceTimerRef.current);
|
||||
}
|
||||
};
|
||||
}, [query, searchField, debounceMs, performSearch]);
|
||||
|
||||
// Manual search function (bypasses debounce)
|
||||
const search = useCallback(async (searchQuery: string, field?: SearchField) => {
|
||||
await performSearch(searchQuery, field || searchField);
|
||||
}, [performSearch, searchField]);
|
||||
|
||||
// Clear search
|
||||
const clearSearch = useCallback(() => {
|
||||
setQuery('');
|
||||
setResults([]);
|
||||
setResultCount(0);
|
||||
setQueryTimeMs(null);
|
||||
setError(null);
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (debounceTimerRef.current) {
|
||||
clearTimeout(debounceTimerRef.current);
|
||||
}
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
query,
|
||||
setQuery,
|
||||
searchField,
|
||||
setSearchField,
|
||||
results,
|
||||
isSearching,
|
||||
error,
|
||||
queryTimeMs,
|
||||
resultCount,
|
||||
embeddingModelUsed,
|
||||
clearSearch,
|
||||
search,
|
||||
};
|
||||
}
|
||||
|
|
@ -505,6 +505,181 @@
|
|||
color: var(--text-primary, #e0e0e0);
|
||||
}
|
||||
|
||||
/* Enhanced Profile Search */
|
||||
.profile-search-enhanced {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* Search Mode Toggle */
|
||||
.search-mode-toggle {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
background: var(--bg-secondary, #f5f5f5);
|
||||
border-radius: 6px;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.search-mode-toggle .mode-btn {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.375rem 0.5rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary, #666);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.search-mode-toggle .mode-btn:hover {
|
||||
background: var(--bg-primary, #fff);
|
||||
color: var(--text-primary, #1a1a2e);
|
||||
}
|
||||
|
||||
.search-mode-toggle .mode-btn.active {
|
||||
background: var(--accent-color, #4f46e5);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.search-mode-toggle .mode-btn svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.dark .search-mode-toggle {
|
||||
background: var(--bg-tertiary, #2a2a4a);
|
||||
}
|
||||
|
||||
.dark .search-mode-toggle .mode-btn {
|
||||
color: var(--text-secondary, #999);
|
||||
}
|
||||
|
||||
.dark .search-mode-toggle .mode-btn:hover {
|
||||
background: var(--bg-secondary, #1a1a2e);
|
||||
color: var(--text-primary, #e0e0e0);
|
||||
}
|
||||
|
||||
/* Search Field Filter Dropdown */
|
||||
.search-field-filter {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.search-field-filter .field-select {
|
||||
width: 100%;
|
||||
padding: 0.375rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
border: 1px solid var(--border-color, #e0e0e0);
|
||||
border-radius: 4px;
|
||||
background: var(--bg-primary, #fff);
|
||||
color: var(--text-primary, #1a1a2e);
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.search-field-filter .field-select:focus {
|
||||
border-color: var(--accent-color, #4f46e5);
|
||||
}
|
||||
|
||||
.dark .search-field-filter .field-select {
|
||||
background: var(--bg-tertiary, #2a2a4a);
|
||||
border-color: var(--border-color, #3a3a5a);
|
||||
color: var(--text-primary, #e0e0e0);
|
||||
}
|
||||
|
||||
/* Search Stats */
|
||||
.search-stats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary, #666);
|
||||
padding: 0 0.25rem;
|
||||
}
|
||||
|
||||
.search-stats .query-time {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Search Error */
|
||||
.search-error {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.375rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--error-color, #dc2626);
|
||||
background: rgba(220, 38, 38, 0.1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Search Spinner */
|
||||
.profile-search .search-spinner {
|
||||
flex-shrink: 0;
|
||||
color: var(--accent-color, #4f46e5);
|
||||
}
|
||||
|
||||
/* Semantic Search Results */
|
||||
.profile-list.semantic-results .profile-item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.profile-item.semantic-result .score-badge {
|
||||
font-size: 0.625rem;
|
||||
padding: 0.125rem 0.375rem;
|
||||
background: var(--accent-color, #4f46e5);
|
||||
color: white;
|
||||
border-radius: 10px;
|
||||
font-weight: 600;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.profile-item .headline-text {
|
||||
font-size: 0.7rem;
|
||||
color: var(--text-secondary, #666);
|
||||
font-style: italic;
|
||||
max-width: 150px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.profile-item .custodian-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
font-size: 0.65rem;
|
||||
padding: 0.125rem 0.375rem;
|
||||
background: var(--bg-tertiary, #e0e0e0);
|
||||
border-radius: 4px;
|
||||
color: var(--text-secondary, #666);
|
||||
}
|
||||
|
||||
.profile-item .location-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
font-size: 0.65rem;
|
||||
color: var(--text-secondary, #666);
|
||||
}
|
||||
|
||||
.dark .profile-item .headline-text {
|
||||
color: var(--text-secondary, #999);
|
||||
}
|
||||
|
||||
.dark .profile-item .custodian-badge {
|
||||
background: var(--bg-secondary, #1a1a2e);
|
||||
color: var(--text-secondary, #999);
|
||||
}
|
||||
|
||||
.profile-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { useState, useEffect, useCallback } from 'react';
|
|||
import { useNavigate } from 'react-router-dom';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
import { Tooltip } from '../components/common/Tooltip';
|
||||
import { usePersonSearch, type SearchField } from '../hooks/usePersonSearch';
|
||||
import {
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
|
|
@ -34,7 +35,9 @@ import {
|
|||
Star,
|
||||
Info,
|
||||
Search,
|
||||
X
|
||||
X,
|
||||
Database,
|
||||
Filter as FilterIcon
|
||||
} from 'lucide-react';
|
||||
|
||||
// Name similarity calculation using Levenshtein distance
|
||||
|
|
@ -270,8 +273,23 @@ export default function EntityReviewPage() {
|
|||
type StatsFilter = 'all' | 'reviewed' | 'pending';
|
||||
const [statsFilter, setStatsFilter] = useState<StatsFilter>('pending');
|
||||
|
||||
// Profile search
|
||||
// Profile search - now with semantic search mode
|
||||
const [profileSearchQuery, setProfileSearchQuery] = useState('');
|
||||
const [useSemanticSearch, setUseSemanticSearch] = useState(false); // Toggle: semantic vs local filter
|
||||
|
||||
// Semantic search hook (searches ALL profiles in vector database)
|
||||
const {
|
||||
query: semanticQuery,
|
||||
setQuery: setSemanticQuery,
|
||||
searchField: semanticSearchField,
|
||||
setSearchField: setSemanticSearchField,
|
||||
results: semanticResults,
|
||||
isSearching: semanticSearching,
|
||||
error: semanticError,
|
||||
queryTimeMs: semanticQueryTime,
|
||||
resultCount: semanticResultCount,
|
||||
clearSearch: clearSemanticSearch,
|
||||
} = usePersonSearch({ debounceMs: 400, minQueryLength: 2, maxResults: 50 });
|
||||
|
||||
// Linkup search state
|
||||
const [linkupSearching, setLinkupSearching] = useState(false);
|
||||
|
|
@ -857,29 +875,159 @@ export default function EntityReviewPage() {
|
|||
<div className="review-content">
|
||||
{/* Profile List Sidebar */}
|
||||
<aside className="profile-sidebar compact">
|
||||
<h2>Profielen ({profileSearchQuery ? `${filteredProfiles.length} / ${profiles.length}` : profiles.length})</h2>
|
||||
<h2>Profielen ({useSemanticSearch && semanticQuery ? `${semanticResultCount} found` : profileSearchQuery ? `${filteredProfiles.length} / ${profiles.length}` : profiles.length})</h2>
|
||||
|
||||
{/* Profile Search */}
|
||||
<div className="profile-search">
|
||||
<Search size={16} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder={language === 'nl' ? 'Zoek op naam of domein...' : 'Search by name or domain...'}
|
||||
value={profileSearchQuery}
|
||||
onChange={(e) => setProfileSearchQuery(e.target.value)}
|
||||
/>
|
||||
{profileSearchQuery && (
|
||||
{/* Enhanced Profile Search */}
|
||||
<div className="profile-search-enhanced">
|
||||
{/* Search Mode Toggle */}
|
||||
<div className="search-mode-toggle">
|
||||
<button
|
||||
className="clear-search"
|
||||
onClick={() => setProfileSearchQuery('')}
|
||||
title={language === 'nl' ? 'Wissen' : 'Clear'}
|
||||
className={`mode-btn ${!useSemanticSearch ? 'active' : ''}`}
|
||||
onClick={() => {
|
||||
setUseSemanticSearch(false);
|
||||
clearSemanticSearch();
|
||||
}}
|
||||
title={language === 'nl' ? 'Filter huidige pagina' : 'Filter current page'}
|
||||
>
|
||||
<X size={14} />
|
||||
<FilterIcon size={14} />
|
||||
<span>{language === 'nl' ? 'Pagina' : 'Page'}</span>
|
||||
</button>
|
||||
<button
|
||||
className={`mode-btn ${useSemanticSearch ? 'active' : ''}`}
|
||||
onClick={() => {
|
||||
setUseSemanticSearch(true);
|
||||
setProfileSearchQuery('');
|
||||
}}
|
||||
title={language === 'nl' ? 'Zoek alle profielen (vector database)' : 'Search all profiles (vector database)'}
|
||||
>
|
||||
<Database size={14} />
|
||||
<span>{language === 'nl' ? 'Alles' : 'All'}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Field Filter (only for semantic search) */}
|
||||
{useSemanticSearch && (
|
||||
<div className="search-field-filter">
|
||||
<select
|
||||
value={semanticSearchField}
|
||||
onChange={(e) => setSemanticSearchField(e.target.value as SearchField)}
|
||||
className="field-select"
|
||||
>
|
||||
<option value="all">{language === 'nl' ? 'Alle velden' : 'All fields'}</option>
|
||||
<option value="name">{language === 'nl' ? 'Naam' : 'Name'}</option>
|
||||
<option value="email">{language === 'nl' ? 'E-mail' : 'Email'}</option>
|
||||
<option value="domain">{language === 'nl' ? 'Domein' : 'Domain'}</option>
|
||||
<option value="birth_year">{language === 'nl' ? 'Geboortejaar' : 'Birth Year'}</option>
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Search Input */}
|
||||
<div className="profile-search">
|
||||
<Search size={16} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder={useSemanticSearch
|
||||
? (language === 'nl' ? 'Zoek in alle profielen...' : 'Search all profiles...')
|
||||
: (language === 'nl' ? 'Filter op naam of domein...' : 'Filter by name or domain...')
|
||||
}
|
||||
value={useSemanticSearch ? semanticQuery : profileSearchQuery}
|
||||
onChange={(e) => {
|
||||
if (useSemanticSearch) {
|
||||
setSemanticQuery(e.target.value);
|
||||
} else {
|
||||
setProfileSearchQuery(e.target.value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{(useSemanticSearch ? semanticQuery : profileSearchQuery) && (
|
||||
<button
|
||||
className="clear-search"
|
||||
onClick={() => {
|
||||
if (useSemanticSearch) {
|
||||
clearSemanticSearch();
|
||||
} else {
|
||||
setProfileSearchQuery('');
|
||||
}
|
||||
}}
|
||||
title={language === 'nl' ? 'Wissen' : 'Clear'}
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
)}
|
||||
{semanticSearching && (
|
||||
<Loader2 className="animate-spin search-spinner" size={14} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Search Stats (semantic mode only) */}
|
||||
{useSemanticSearch && semanticQuery && !semanticSearching && semanticResultCount > 0 && (
|
||||
<div className="search-stats">
|
||||
<span>{semanticResultCount} {language === 'nl' ? 'resultaten' : 'results'}</span>
|
||||
{semanticQueryTime && <span className="query-time">({semanticQueryTime.toFixed(0)}ms)</span>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Semantic Search Error */}
|
||||
{semanticError && (
|
||||
<div className="search-error">
|
||||
<AlertCircle size={14} />
|
||||
<span>{semanticError}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
{/* Semantic Search Results */}
|
||||
{useSemanticSearch && semanticQuery && !semanticSearching && semanticResults.length > 0 ? (
|
||||
<ul className="profile-list semantic-results">
|
||||
{semanticResults.map((result, idx) => (
|
||||
<li
|
||||
key={result.ppid || `semantic-${idx}`}
|
||||
className="profile-item semantic-result"
|
||||
onClick={() => {
|
||||
// If the result has a ppid, try to fetch that profile
|
||||
if (result.ppid) {
|
||||
fetchProfileDetail(result.ppid);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="profile-item-header">
|
||||
<User size={16} />
|
||||
<span className="profile-name">{result.name}</span>
|
||||
{result.score && (
|
||||
<span className="score-badge" title="Match score">
|
||||
{(result.score * 100).toFixed(0)}%
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="profile-item-meta">
|
||||
{result.headline && (
|
||||
<span className="headline-text" title={result.headline}>
|
||||
{result.headline.length > 40 ? result.headline.slice(0, 40) + '...' : result.headline}
|
||||
</span>
|
||||
)}
|
||||
{result.custodian_name && (
|
||||
<span className="custodian-badge">
|
||||
<Building2 size={12} />
|
||||
{result.custodian_name}
|
||||
</span>
|
||||
)}
|
||||
{result.location && (
|
||||
<span className="location-badge">
|
||||
<Globe size={12} />
|
||||
{result.location}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : useSemanticSearch && semanticQuery && !semanticSearching && semanticResults.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
<Search size={32} />
|
||||
<span>{language === 'nl' ? 'Geen resultaten gevonden' : 'No results found'}</span>
|
||||
</div>
|
||||
) : loading ? (
|
||||
<div className="loading-state">
|
||||
<Loader2 className="animate-spin" size={24} />
|
||||
<span>{t('loading')}</span>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"generated": "2026-01-14T21:38:51.740Z",
|
||||
"generated": "2026-01-14T21:57:09.847Z",
|
||||
"schemaRoot": "/schemas/20251121/linkml",
|
||||
"totalFiles": 3026,
|
||||
"categoryCounts": {
|
||||
|
|
|
|||
|
|
@ -143,7 +143,9 @@ imports:
|
|||
- ../slots/url
|
||||
- ../slots/validation_status
|
||||
- ../slots/wikidata
|
||||
- ../slots/wikidata_class
|
||||
# REMOVED: ../slots/wikidata_class - migrated to is_or_was_instance_of with WikiDataEntry (2026-01-14, Rule 53)
|
||||
- ../slots/is_or_was_instance_of
|
||||
- ./WikiDataEntry
|
||||
- ../slots/has_or_had_restriction
|
||||
- ./Restriction
|
||||
- ./FindingAid
|
||||
|
|
@ -268,7 +270,8 @@ classes:
|
|||
- url
|
||||
- temporal_extent # was: valid_from + valid_to - migrated per Rule 53
|
||||
- has_or_had_web_claim
|
||||
- wikidata_class
|
||||
# REMOVED: wikidata_class - migrated to is_or_was_instance_of with WikiDataEntry (2026-01-14, Rule 53)
|
||||
- is_or_was_instance_of
|
||||
slot_usage:
|
||||
id:
|
||||
identifier: true
|
||||
|
|
|
|||
|
|
@ -23,7 +23,9 @@ imports:
|
|||
# REMOVED: ../slots/verified_by - migrated to is_or_was_verified_by with Verifier (2026-01-14, Rule 53)
|
||||
- ../slots/is_or_was_verified_by
|
||||
- ./Verifier
|
||||
- ../slots/word_count
|
||||
# REMOVED: ../slots/word_count - migrated to has_or_had_quantity with WordCount (2026-01-14, Rule 53)
|
||||
- ../slots/has_or_had_quantity
|
||||
- ./WordCount
|
||||
- ./SpecificityAnnotation
|
||||
- ./TemplateSpecificityScores
|
||||
- ../enums/GenerationMethodEnum
|
||||
|
|
@ -89,7 +91,8 @@ classes:
|
|||
- verification_date
|
||||
# REMOVED: verified_by - migrated to is_or_was_verified_by with Verifier (2026-01-14, Rule 53)
|
||||
- is_or_was_verified_by
|
||||
- word_count
|
||||
# REMOVED: word_count - migrated to has_or_had_quantity with WordCount (2026-01-14, Rule 53)
|
||||
- has_or_had_quantity
|
||||
slot_usage:
|
||||
source_video:
|
||||
range: string
|
||||
|
|
@ -206,12 +209,26 @@ classes:
|
|||
examples:
|
||||
- value: 45.3
|
||||
description: Processed in 45.3 seconds
|
||||
word_count:
|
||||
range: integer
|
||||
# DEPRECATED: word_count - migrated to has_or_had_quantity with WordCount (2026-01-14, Rule 53)
|
||||
# word_count:
|
||||
# range: integer
|
||||
# required: false
|
||||
# minimum_value: 0
|
||||
# examples:
|
||||
# - value: 1523
|
||||
# description: 1,523 words in transcript
|
||||
has_or_had_quantity:
|
||||
range: WordCount
|
||||
required: false
|
||||
minimum_value: 0
|
||||
inlined: true
|
||||
description: |
|
||||
Word count in the transcript.
|
||||
MIGRATED from word_count slot (2026-01-14) per Rule 53.
|
||||
|
||||
Uses WordCount class for structured quantity with value.
|
||||
examples:
|
||||
- value: 1523
|
||||
- value: |
|
||||
value: 1523
|
||||
description: 1,523 words in transcript
|
||||
character_count:
|
||||
range: integer
|
||||
|
|
|
|||
|
|
@ -14,8 +14,12 @@ imports:
|
|||
# REMOVED: ../slots/wikidata_entity_id - migrated to has_or_had_identifier with WikiDataIdentifier (2026-01-14, Rule 53)
|
||||
- ../slots/has_or_had_identifier
|
||||
- ./WikiDataIdentifier
|
||||
- ../slots/wikidata_entity_label
|
||||
- ../slots/wikidata_mapping_rationale
|
||||
# REMOVED: ../slots/wikidata_entity_label - migrated to has_or_had_label with Label (2026-01-14, Rule 53)
|
||||
- ../slots/has_or_had_label
|
||||
- ./Label
|
||||
# REMOVED: ../slots/wikidata_mapping_rationale - migrated to has_or_had_rationale with Rationale (2026-01-14, Rule 53)
|
||||
- ../slots/has_or_had_rationale
|
||||
- ./Rationale
|
||||
- ../slots/has_or_had_type
|
||||
classes:
|
||||
WikidataAlignment:
|
||||
|
|
@ -26,9 +30,11 @@ classes:
|
|||
slots:
|
||||
# REMOVED: wikidata_entity_id - migrated to has_or_had_identifier with WikiDataIdentifier (2026-01-14, Rule 53)
|
||||
- has_or_had_identifier
|
||||
- wikidata_entity_label
|
||||
# REMOVED: wikidata_entity_label - migrated to has_or_had_label with Label (2026-01-14, Rule 53)
|
||||
- has_or_had_label
|
||||
- has_or_had_type
|
||||
- wikidata_mapping_rationale
|
||||
# REMOVED: wikidata_mapping_rationale - migrated to has_or_had_rationale with Rationale (2026-01-14, Rule 53)
|
||||
- has_or_had_rationale
|
||||
slot_usage:
|
||||
has_or_had_identifier:
|
||||
range: WikiDataIdentifier
|
||||
|
|
@ -42,6 +48,29 @@ classes:
|
|||
qid: Q27032435
|
||||
label: "academic archive"
|
||||
description: Wikidata Q-number with optional label
|
||||
has_or_had_label:
|
||||
range: Label
|
||||
description: |
|
||||
Human-readable label for the Wikidata entity.
|
||||
MIGRATED from wikidata_entity_label slot (2026-01-14) per Rule 53.
|
||||
|
||||
Uses Label class for structured label with language code.
|
||||
examples:
|
||||
- value: |
|
||||
label_value: "academic archive"
|
||||
language_code: "en"
|
||||
description: English label for the Wikidata entity
|
||||
has_or_had_rationale:
|
||||
range: Rationale
|
||||
description: |
|
||||
Rationale for the mapping between local class and Wikidata entity.
|
||||
MIGRATED from wikidata_mapping_rationale slot (2026-01-14) per Rule 53.
|
||||
|
||||
Uses Rationale class for structured rationale text.
|
||||
examples:
|
||||
- value: |
|
||||
rationale_text: "AcademicArchive is semantically equivalent to Q27032435"
|
||||
description: Mapping rationale
|
||||
has_or_had_type:
|
||||
range: MappingType
|
||||
description: |
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue