Skip to content

Commit b0fee2b

Browse files
Support search by subjects
1 parent f449868 commit b0fee2b

3 files changed

Lines changed: 285 additions & 3 deletions

File tree

app.js

Lines changed: 164 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -988,6 +988,7 @@
988988
const storyRoot = document.getElementById("__ml_story_recit_root");
989989
if (storyRoot && typeof renderRecit === "function") {
990990
storyRoot.innerHTML = renderRecit() || "";
991+
renderStorySubjectFallback(storyRoot);
991992
}
992993
} catch (e) {
993994
console.warn("story recit render error:", e);
@@ -1005,9 +1006,132 @@
10051006
} catch (e) {
10061007
console.warn("theme grid render error:", e);
10071008
}
1009+
renderExplorerFocus();
10081010
}
10091011
window.renderStoryTier = renderStoryTier;
10101012

1013+
function escapeHtml(value) {
1014+
return String(value || "")
1015+
.replace(/&/g, "&")
1016+
.replace(/</g, "&lt;")
1017+
.replace(/>/g, "&gt;")
1018+
.replace(/"/g, "&quot;")
1019+
.replace(/'/g, "&#039;");
1020+
}
1021+
1022+
function explorerRelatedWorks(snapshot, selectedId) {
1023+
const graph = snapshot.graphe || {};
1024+
const nodes = graph.noeuds || [];
1025+
const relations = graph.relations || [];
1026+
const nodeById = new Map(nodes.map((node) => [node.id, node]));
1027+
return relations
1028+
.filter((relation) => relation.source === selectedId && ["subject_of", "represente", "houses_work", "expose"].includes(relation.type))
1029+
.map((relation) => nodeById.get(relation.target))
1030+
.filter(Boolean)
1031+
.slice(0, 12);
1032+
}
1033+
1034+
function subjectSelectionFromSnapshot(snapshot) {
1035+
const selectedId = snapshot.entite_selectionnee_id || "";
1036+
const selected = snapshot.entite_selectionnee || {};
1037+
const selectedRecord = selected.donnees || {};
1038+
const selectedType = normaliseDetailEntityType(selected.type || snapshot.entite_selectionnee_type || "");
1039+
if (!selectedId || (selectedType !== "sujet" && selectedType !== "subject")) {
1040+
return null;
1041+
}
1042+
const label = fieldValue(selectedRecord, "subjectLabel") || fieldValue(selectedRecord, "label") || runtimeState.selectedEntity.name || selectedId;
1043+
return { selectedId, selectedRecord, selectedType, label };
1044+
}
1045+
1046+
function renderStorySubjectFallback(root) {
1047+
const snapshot = currentSnapshot();
1048+
const subject = subjectSelectionFromSnapshot(snapshot);
1049+
if (!subject) {
1050+
return;
1051+
}
1052+
1053+
const relatedWorks = explorerRelatedWorks(snapshot, subject.selectedId);
1054+
const count = Number(subject.selectedRecord.artworkCount || relatedWorks.length || 0);
1055+
let html = '<article class="recit-card recit-sujet story-subject-result">';
1056+
html += '<header class="recit-header">';
1057+
html += '<span class="recit-badge">Theme</span>';
1058+
html += '<h2 class="recit-titre">' + escapeHtml(subject.label || subject.selectedId) + '</h2>';
1059+
html += '</header>';
1060+
html += '<div class="recit-corps">';
1061+
html += '<p class="recit-intro">' + (count ? escapeHtml(count + " artworks found for this theme.") : "Loading artworks for this theme...") + '</p>';
1062+
if (relatedWorks.length) {
1063+
html += '<ul class="recit-liste-oeuvres">';
1064+
relatedWorks.slice(0, 10).forEach((work) => {
1065+
html += '<li class="recit-oeuvre">' + escapeHtml(work.etiquette || work.label || work.id) + '</li>';
1066+
});
1067+
html += '</ul>';
1068+
}
1069+
html += '</div>';
1070+
html += '<footer class="recit-footer">';
1071+
html += '<button type="button" class="recit-btn-explorer" data-explorer-open-observatory>Explore in constellation</button>';
1072+
html += '</footer>';
1073+
html += '</article>';
1074+
root.innerHTML = html;
1075+
}
1076+
1077+
function renderExplorerFocus() {
1078+
const root = document.getElementById("__ml_explorer_focus_root");
1079+
if (!root) {
1080+
return;
1081+
}
1082+
1083+
const snapshot = currentSnapshot();
1084+
const selectedId = snapshot.entite_selectionnee_id || "";
1085+
const selected = snapshot.entite_selectionnee || {};
1086+
const selectedRecord = selected.donnees || {};
1087+
const selectedType = normaliseDetailEntityType(selected.type || snapshot.entite_selectionnee_type || "");
1088+
const label = fieldValue(selectedRecord, "subjectLabel") || fieldValue(selectedRecord, "museumLabel") || fieldValue(selectedRecord, "movementLabel") || fieldValue(selectedRecord, "artistLabel") || fieldValue(selectedRecord, "artworkLabel") || fieldValue(selectedRecord, "label") || runtimeState.selectedEntity.name || selectedId;
1089+
1090+
if (!selectedId) {
1091+
root.innerHTML = [
1092+
'<div class="explorer-empty">',
1093+
'<strong>Choose a theme or search result</strong>',
1094+
'<span>Explorer will show the selected entity and its first useful connections here.</span>',
1095+
'</div>'
1096+
].join("");
1097+
return;
1098+
}
1099+
1100+
const relatedWorks = explorerRelatedWorks(snapshot, selectedId);
1101+
const count = Number(selectedRecord.artworkCount || relatedWorks.length || 0);
1102+
let html = '<article class="explorer-focus-card">';
1103+
html += '<header class="explorer-focus-header">';
1104+
html += '<div><span class="eyebrow">Current selection</span>';
1105+
html += '<h3>' + escapeHtml(label || selectedId) + '</h3></div>';
1106+
html += '<span class="chip">' + escapeHtml(selectedType || "entity") + '</span>';
1107+
html += '</header>';
1108+
1109+
if (selectedType === "sujet" || selectedType === "subject") {
1110+
html += '<p class="explorer-focus-summary">' + (count ? escapeHtml(count + " artworks found for this theme.") : "Theme loaded. Try another theme if Wikidata has no direct artwork matches.") + '</p>';
1111+
} else {
1112+
html += '<p class="explorer-focus-summary">' + escapeHtml(runtimeState.selectedEntity.meta || "Selection loaded.") + '</p>';
1113+
}
1114+
1115+
if (relatedWorks.length) {
1116+
html += '<div class="explorer-work-list" aria-label="Related artworks">';
1117+
relatedWorks.forEach((work) => {
1118+
html += '<button type="button" class="explorer-work-item" data-explorer-entity-id="' + escapeHtml(work.id) + '" data-explorer-entity-type="artwork">';
1119+
html += '<strong>' + escapeHtml(work.etiquette || work.label || work.id) + '</strong>';
1120+
html += '<small>artwork</small>';
1121+
html += '</button>';
1122+
});
1123+
html += '</div>';
1124+
}
1125+
1126+
html += '<div class="explorer-focus-actions">';
1127+
html += '<button type="button" class="ghost-button" data-explorer-open-observatory>See constellation</button>';
1128+
html += '</div>';
1129+
html += '</article>';
1130+
root.innerHTML = html;
1131+
}
1132+
1133+
window.renderExplorerFocus = renderExplorerFocus;
1134+
10111135
function updateTierInUrl(tier) {
10121136
if (!window.history || typeof window.history.replaceState !== "function") {
10131137
return;
@@ -1042,6 +1166,7 @@ function setActiveTier(tier, options = {}) {
10421166
}
10431167
} else if (tier === "explorer") {
10441168
renderStoryTier();
1169+
renderExplorerFocus();
10451170
} else if (tier === "observatory") {
10461171
const guideKey = "observatory-guide-seen";
10471172
if (!localStorage.getItem(guideKey)) {
@@ -3245,6 +3370,7 @@ function setActiveTier(tier, options = {}) {
32453370
if (typeof window.renderDetailPanel === "function") {
32463371
window.renderDetailPanel();
32473372
}
3373+
renderExplorerFocus();
32483374
}
32493375

32503376
function loadEntityByNode(node) {
@@ -4565,20 +4691,57 @@ function setActiveTier(tier, options = {}) {
45654691
if (!tile) return;
45664692
const sujetId = tile.dataset.sujetId;
45674693
if (!sujetId) return;
4694+
tile.classList.add("is-loading");
45684695
try {
4569-
if (typeof charger_sujet === "function") {
4696+
const storyRoot = document.getElementById("__ml_story_recit_root");
4697+
const subjectLabel = tile.dataset.sujetLabel || tile.querySelector(".theme-tile-label")?.textContent || tile.textContent.trim() || sujetId;
4698+
if (storyRoot && document.body?.dataset.tier === "story") {
4699+
storyRoot.innerHTML = '<article class="recit-card recit-sujet story-subject-result"><header class="recit-header"><span class="recit-badge">Theme</span><h2 class="recit-titre">' + escapeHtml(subjectLabel) + '</h2></header><div class="recit-corps"><p class="recit-intro">Loading artworks for this theme...</p></div></article>';
4700+
}
4701+
if (window.ui?.etat?.amorcer_entite) {
4702+
window.ui.etat.amorcer_entite(sujetId, "sujet", subjectLabel);
4703+
}
4704+
if (window.ui?.etat?.charger_sujet) {
4705+
await invokeRuntimeAction("charger_sujet", window.ui.etat.charger_sujet.bind(window.ui.etat), [sujetId]);
4706+
} else if (typeof charger_sujet === "function") {
45704707
await charger_sujet(sujetId);
45714708
}
45724709
renderConstellation();
45734710
renderRuntimeState();
45744711
await refreshMultilingualEntityLabels();
45754712
window.renderDetailPanel?.();
45764713
window.renderStoryTier?.();
4714+
window.renderExplorerFocus?.();
45774715
} catch (err) {
45784716
console.warn("charger-sujet error:", err);
4717+
} finally {
4718+
tile.classList.remove("is-loading");
4719+
}
4720+
});
4721+
4722+
document.addEventListener("click", async (e) => {
4723+
const item = e.target.closest("[data-explorer-entity-id]");
4724+
if (!item) return;
4725+
const id = item.dataset.explorerEntityId;
4726+
const type = item.dataset.explorerEntityType || "artwork";
4727+
if (!id) return;
4728+
try {
4729+
if (typeof window.loadSearchSelection === "function") {
4730+
await window.loadSearchSelection(id, type, item.textContent.trim() || id);
4731+
} else {
4732+
await loadEntityByNode({ id, type });
4733+
}
4734+
renderExplorerFocus();
4735+
} catch (err) {
4736+
console.warn("explorer entity load error:", err);
45794737
}
45804738
});
45814739

4740+
document.addEventListener("click", (e) => {
4741+
if (!e.target.closest("[data-explorer-open-observatory]")) return;
4742+
setActiveTier("observatory");
4743+
});
4744+
45824745
// Search mode toggle in Explorer panel
45834746
let currentSearchMode = "entity";
45844747
window.getActiveSearchMode = function getActiveSearchMode() {

index.html

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,11 +150,15 @@ <h2>Search, compare, and find connections.</h2>
150150
<div class="search-modes" role="group" aria-label="Choose search type">
151151
<button type="button" class="search-mode-btn is-active" data-search-mode="entity" title="Search for any named entity">Find entity</button>
152152
<button type="button" class="search-mode-btn" data-search-mode="subject" title="Find artworks that depict a subject">Artworks of&hellip;</button>
153-
<button type="button" class="search-mode-btn" data-search-mode="nationality" title="Find artists by nationality">Artists from&hellip;</button>
154-
<button type="button" class="search-mode-btn" data-search-mode="material" title="Find artworks by material or technique">By material&hellip;</button>
153+
<button type="button" class="search-mode-btn" data-search-mode="nationality" title="Artists by nationality — coming next" disabled>Artists from&hellip;</button>
154+
<button type="button" class="search-mode-btn" data-search-mode="material" title="Artworks by material — coming next" disabled>By material&hellip;</button>
155155
</div>
156156
<p class="search-mode-hint" id="search-mode-hint" role="status" aria-live="polite">Search for a movement, artist, or artwork using the bar above.</p>
157157

158+
<section class="explorer-focus" id="explorer-focus-section" aria-label="Current explorer result">
159+
<div id="__ml_explorer_focus_root"></div>
160+
</section>
161+
158162
<div class="explorer-layout">
159163
<!-- Theme grid for zero-typing discovery -->
160164
<section class="explorer-themes" aria-label="Browse by theme">

styles.css

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3376,6 +3376,12 @@
33763376
transform: translateY(-2px);
33773377
}
33783378

3379+
.theme-tile.is-loading {
3380+
border-color: var(--accent-cyan);
3381+
box-shadow: 0 0 0 3px rgba(110, 243, 255, 0.12);
3382+
opacity: 0.82;
3383+
}
3384+
33793385
.theme-tile:focus-visible {
33803386
outline: var(--focus-ring);
33813387
outline-offset: var(--focus-ring-offset);
@@ -3453,6 +3459,17 @@
34533459
color: var(--text-main);
34543460
}
34553461

3462+
.search-mode-btn:disabled {
3463+
cursor: not-allowed;
3464+
opacity: 0.46;
3465+
}
3466+
3467+
.search-mode-btn:disabled:hover {
3468+
border-color: var(--line-soft);
3469+
background: var(--bg-soft);
3470+
color: var(--text-soft);
3471+
}
3472+
34563473
.search-mode-btn:focus-visible {
34573474
outline: var(--focus-ring);
34583475
outline-offset: var(--focus-ring-offset);
@@ -3465,6 +3482,104 @@
34653482
margin: 0 0 18px;
34663483
}
34673484

3485+
.explorer-focus {
3486+
margin-bottom: 20px;
3487+
}
3488+
3489+
.explorer-empty,
3490+
.explorer-focus-card {
3491+
border: 1.5px solid var(--line-soft);
3492+
border-radius: var(--radius-md);
3493+
background: var(--bg-soft);
3494+
padding: 16px;
3495+
}
3496+
3497+
.explorer-empty {
3498+
display: flex;
3499+
flex-direction: column;
3500+
gap: 4px;
3501+
color: var(--text-soft);
3502+
}
3503+
3504+
.explorer-empty strong {
3505+
color: var(--text-main);
3506+
}
3507+
3508+
.explorer-focus-header {
3509+
display: flex;
3510+
justify-content: space-between;
3511+
gap: 16px;
3512+
align-items: flex-start;
3513+
margin-bottom: 10px;
3514+
}
3515+
3516+
.explorer-focus-header h3 {
3517+
margin: 4px 0 0;
3518+
font-size: 1.2rem;
3519+
line-height: 1.25;
3520+
color: var(--text-main);
3521+
}
3522+
3523+
.explorer-focus-summary {
3524+
margin: 0 0 14px;
3525+
color: var(--text-soft);
3526+
font-size: 0.95rem;
3527+
}
3528+
3529+
.explorer-work-list {
3530+
display: grid;
3531+
grid-template-columns: repeat(4, minmax(0, 1fr));
3532+
gap: 10px;
3533+
margin-top: 12px;
3534+
}
3535+
3536+
@media (max-width: 900px) {
3537+
.explorer-work-list {
3538+
grid-template-columns: repeat(2, minmax(0, 1fr));
3539+
}
3540+
}
3541+
3542+
.explorer-work-item {
3543+
min-height: 74px;
3544+
padding: 10px 12px;
3545+
border: 1.5px solid var(--line-soft);
3546+
border-radius: var(--radius-sm);
3547+
background: rgba(255, 255, 255, 0.04);
3548+
color: var(--text-main);
3549+
cursor: pointer;
3550+
text-align: left;
3551+
transition: background var(--transition-fast), border-color var(--transition-fast), transform var(--transition-fast);
3552+
}
3553+
3554+
.explorer-work-item:hover,
3555+
.explorer-work-item:focus-visible {
3556+
border-color: var(--accent-teal);
3557+
background: var(--bg-panel-strong);
3558+
transform: translateY(-1px);
3559+
}
3560+
3561+
.explorer-work-item strong {
3562+
display: block;
3563+
font-size: 0.86rem;
3564+
line-height: 1.25;
3565+
}
3566+
3567+
.explorer-work-item small {
3568+
display: block;
3569+
margin-top: 6px;
3570+
color: var(--text-muted);
3571+
font-size: 0.72rem;
3572+
text-transform: uppercase;
3573+
letter-spacing: 0.05em;
3574+
}
3575+
3576+
.explorer-focus-actions {
3577+
display: flex;
3578+
gap: 10px;
3579+
margin-top: 14px;
3580+
flex-wrap: wrap;
3581+
}
3582+
34683583
/* Explorer layout */
34693584
.explorer-layout {
34703585
display: grid;

0 commit comments

Comments
 (0)