diff --git a/unredact/static/app.js b/unredact/static/app.js index fc2775f..dc81383 100644 --- a/unredact/static/app.js +++ b/unredact/static/app.js @@ -909,6 +909,56 @@ function tierLabel(tier) { return "T3"; } +function tierDescription(tier) { + if (tier === 1) return "Flight logs — traveled with Epstein"; + if (tier === 2) return "Inner circle — staff, financial, or frequently named"; + return "Named in Epstein case files"; +} + +function showAssocDetail(assocMatches, anchorEl) { + // Remove any existing popup + const old = document.getElementById("assoc-detail"); + if (old) old.remove(); + + const popup = document.createElement("div"); + popup.id = "assoc-detail"; + + let html = '
Possible associates
'; + html += '
'; + + for (const m of assocMatches) { + const cls = tierBadgeClass(m.tier); + html += `
+ ${tierLabel(m.tier)} +
+
${escapeHtml(m.personName)}
+
${escapeHtml(tierDescription(m.tier))} · ${escapeHtml(m.category)} · matched on ${escapeHtml(m.matchType)}
+
+
`; + } + + html += '
'; + popup.innerHTML = html; + + // Position near the badge + solvePanel.appendChild(popup); + + // Close handlers + popup.querySelector(".assoc-detail-close").addEventListener("click", (e) => { + e.stopPropagation(); + popup.remove(); + }); + + // Close on outside click + const closeOnOutside = (e) => { + if (!popup.contains(e.target)) { + popup.remove(); + document.removeEventListener("click", closeOnOutside, true); + } + }; + setTimeout(() => document.addEventListener("click", closeOnOutside, true), 0); +} + solveStart.addEventListener("click", startSolve); solveStop.addEventListener("click", stopSolve); @@ -1020,18 +1070,23 @@ function handleSolveEvent(data, gapIdx) { div.dataset.assocScore = topMatch.score; } - let badgeHtml = ""; - if (topMatch) { - const cls = tierBadgeClass(topMatch.tier); - const tooltip = `${topMatch.personName} (${topMatch.category}) — ${topMatch.matchType} match`; - badgeHtml = `${tierLabel(topMatch.tier)}`; - } - div.innerHTML = ` - ${badgeHtml} ${escapeHtml(data.text)} ${data.error_px.toFixed(1)}px ${data.source || ""} `; + + if (assocMatches.length > 0) { + const badge = document.createElement("button"); + badge.className = `assoc-badge ${tierBadgeClass(topMatch.tier)}`; + badge.textContent = tierLabel(topMatch.tier); + badge.title = "Click for details"; + badge.addEventListener("click", (e) => { + e.stopPropagation(); + showAssocDetail(assocMatches, badge); + }); + div.prepend(badge); + } + div.addEventListener("click", () => { const override = ensureOverride(); override.gapPreviews[gapIdx] = data.text; diff --git a/unredact/static/style.css b/unredact/static/style.css index a807e3d..0038e12 100644 --- a/unredact/static/style.css +++ b/unredact/static/style.css @@ -487,16 +487,23 @@ main { font-size: 0.75rem; } -/* Associate badges */ +/* Associate badges (clickable) */ .assoc-badge { flex-shrink: 0; - padding: 1px 5px; + padding: 2px 6px; border-radius: 3px; font-size: 0.6rem; font-weight: 700; letter-spacing: 0.05em; - cursor: default; + cursor: pointer; margin-right: 4px; + border: 1px solid transparent; + transition: filter 0.15s, border-color 0.15s; +} + +.assoc-badge:hover { + filter: brightness(1.3); + border-color: #fff; } .assoc-badge.tier-1 { @@ -513,3 +520,81 @@ main { background: #f9a825; color: #000; } + +/* Associate detail popup */ +#assoc-detail { + position: absolute; + top: 80px; + left: 12px; + right: 12px; + max-height: 60%; + background: rgba(16, 16, 32, 0.97); + border: 1px solid #444; + border-radius: 8px; + padding: 10px; + z-index: 40; + overflow-y: auto; + backdrop-filter: blur(10px); +} + +.assoc-detail-header { + display: flex; + justify-content: space-between; + align-items: center; + font-weight: bold; + color: #00d474; + margin-bottom: 8px; + font-size: 0.85rem; +} + +.assoc-detail-close { + background: #0f3460; + color: #e0e0e0; + border: 1px solid #1a3a6e; + border-radius: 4px; + width: 1.5rem; + height: 1.5rem; + cursor: pointer; + font-size: 0.75rem; + display: flex; + align-items: center; + justify-content: center; +} + +.assoc-detail-list { + display: flex; + flex-direction: column; + gap: 6px; +} + +.assoc-detail-item { + display: flex; + align-items: flex-start; + gap: 8px; + padding: 6px 8px; + background: rgba(255, 255, 255, 0.04); + border-radius: 4px; +} + +.assoc-detail-item .assoc-badge { + margin-top: 2px; + cursor: default; + pointer-events: none; +} + +.assoc-detail-info { + flex: 1; + min-width: 0; +} + +.assoc-detail-name { + color: #e0e0e0; + font-weight: 600; + font-size: 0.85rem; +} + +.assoc-detail-meta { + color: #888; + font-size: 0.7rem; + margin-top: 2px; +}