Compare commits

...

2 Commits

Author SHA1 Message Date
Brett caf9123cc5 navigation 2025-07-07 22:01:27 -04:00
Brett ccd01264ae security 2025-07-07 21:29:01 -04:00
2 changed files with 183 additions and 6 deletions

View File

@ -337,6 +337,10 @@ async def on_message(message: discord.Message) -> None:
if not (is_dm or is_mention): if not (is_dm or is_mention):
return return
if is_dm and message.author.id != 199680010267656192:
await message.channel.send("Only authorized users are allowed to use this bot.")
return
url = extract_first_url(message.content) url = extract_first_url(message.content)
if not url: if not url:
await message.channel.send("Please send me a link to a news article.") await message.channel.send("Please send me a link to a news article.")

View File

@ -100,7 +100,13 @@
max-width:900px; max-width:900px;
margin:0 auto; margin:0 auto;
} }
/* make each paragraph a little flex-box so we can place
the coloured badge on the right-hand side */
.paragraph-card{ .paragraph-card{
display:flex;
justify-content:space-between;
align-items:flex-start;
gap:.75rem;
background:var(--card-bg); background:var(--card-bg);
border-radius:6px; border-radius:6px;
padding:1rem 1.2rem; padding:1rem 1.2rem;
@ -110,12 +116,63 @@
.paragraph-card:hover{ .paragraph-card:hover{
box-shadow:0 3px 8px rgba(0,0,0,.14); box-shadow:0 3px 8px rgba(0,0,0,.14);
} }
.paragraph-card--active{
outline:3px solid #1565c0; /* blue focus ring */
outline-offset:2px;
box-shadow:0 0 6px rgba(21,101,192,.35); /* soft glow */
}
/* ─────────── Relevance badge ─────────── */
.relevance-badge{
min-width:72px;
font-size:.75rem;
font-weight:600;
line-height:1.4;
color:#fff;
padding:.25rem .4rem;
border-radius:4px;
text-align:center;
user-select:none;
flex-shrink:0;
}
.relevance-low {background:#d32f2f;} /* red */
.relevance-med {background:#f57c00;} /* amber */
.relevance-high {background:#388e3c;} /* green */
.error{ .error{
color:#c00; color:#c00;
text-align:center; text-align:center;
margin-top:2rem; margin-top:2rem;
} }
/* ─────────── Floating navigation arrows ─────────── */
.nav-arrow{
position:fixed;
top:50%;
transform:translateY(-50%);
width:42px;
height:42px;
border:none;
border-radius:50%;
background:#0d47a1;
color:#fff;
font-size:1.35rem;
line-height:1;
display:flex;
align-items:center;
justify-content:center;
cursor:pointer;
box-shadow:0 2px 6px rgba(0,0,0,.25);
z-index:110;
transition:background .2s ease;
}
.nav-arrow:hover{
background:#1565c0;
}
.nav-arrow:disabled{
opacity:.35;
cursor:default;
}
.nav-arrow--left {left:.75rem;}
.nav-arrow--right{right:.75rem;}
</style> </style>
</head> </head>
@ -209,20 +266,136 @@
const card = document.createElement('div'); const card = document.createElement('div');
card.className = 'paragraph-card'; card.className = 'paragraph-card';
/* store ratings for future use */ /* main paragraph text */
card.dataset.summaryRating = pData.summary_rating ?? '';
card.dataset.topicRatings = JSON.stringify(
(pData.topic_ratings ?? []).map(r => !!r.rating)
);
const p = document.createElement('p'); const p = document.createElement('p');
p.textContent = pData.text; p.textContent = pData.text;
p.style.margin = 0;
card.appendChild(p); card.appendChild(p);
/* relevance badge --------------------------------------- */
const badge = document.createElement('span');
badge.classList.add('relevance-badge');
/* fall-back to 0 if the API did not supply a number */
const summary_rating = Number.parseFloat(pData.summary_rating ?? 0) / 100;
const ratingsArr = pData.topic_ratings ?? [];
const totalTrue = ratingsArr.reduce(
(sum, r) => sum + (r.rating ? 1 : 0),
0
);
const topic_ratings = totalTrue / ratingsArr.length;
const rating = (summary_rating + topic_ratings) / 2;
let cssClass = 'relevance-low';
if (rating >= 0.66) cssClass = 'relevance-high';
else if (rating >= 0.33) cssClass = 'relevance-med';
badge.classList.add(cssClass);
/* display as percentage for clarity */
const pct = Math.round(rating * 100);
badge.textContent = pct + '% relevant';
card.appendChild(badge);
card.dataset.summaryRating = rating; /* keep numeric value for sorting */
elParagraphs.appendChild(card); elParagraphs.appendChild(card);
}); });
setupRelevanceNavigation();
} }
/* ───────────────── Floating-arrow navigation ───────────────── */
let sortedCards = []; // cards sorted by relevance
let currentIdx = -1; // index of the active card
let previousIdx = -1;
let arrowPrev, arrowNext; // will be assigned in setup
function setupRelevanceNavigation(){
const cards = Array.from(document.querySelectorAll('.paragraph-card'));
if(!cards.length) return;
sortedCards = cards.sort(
(a,b) => parseFloat(b.dataset.summaryRating) - parseFloat(a.dataset.summaryRating)
);
/* grab the arrow buttons so helpers can reach them */
arrowPrev = document.getElementById('arrowPrev');
arrowNext = document.getElementById('arrowNext');
arrowPrev.addEventListener('click', () => goTo(currentIdx - 1));
arrowNext.addEventListener('click', () => goTo(currentIdx + 1));
/* start on the most-relevant paragraph */
// goTo(0, /*smooth*/false);
updateArrowState();
}
function goTo(idx, smooth = true, fromScroll = false){
if (idx < 0)
idx = sortedCards.length - 1;
else if (idx >= sortedCards.length)
idx = 0;
// if(idx < 0 || idx >= sortedCards.length) return;
currentIdx = idx;
/* only scroll the viewport when the user clicked an arrow */
if(!fromScroll){
sortedCards[currentIdx].scrollIntoView({
behavior: smooth ? 'smooth' : 'auto',
block: 'center'
});
}
updateArrowState();
highlightActive();
}
function updateArrowState(){
if (currentIdx < 0) {
arrowPrev.disabled = false;
arrowNext.disabled = false;
return;
}
arrowPrev.disabled = currentIdx === 0;
arrowNext.disabled = currentIdx === sortedCards.length - 1;
}
function highlightActive(){
if (currentIdx < 0) return;
if(previousIdx !== -1){
sortedCards[previousIdx].classList.remove('paragraph-card--active');
}
sortedCards[currentIdx].classList.add('paragraph-card--active');
previousIdx = currentIdx;
}
document.addEventListener('keydown', evt => {
/* ignore key presses while the user is typing in inputs / textareas */
const tag = (evt.target.tagName || '').toLowerCase();
if (tag === 'input' || tag === 'textarea' || evt.target.isContentEditable) {
return;
}
if (evt.key === 'ArrowLeft') {
/* same as clicking the left arrow */
if (typeof arrowPrev !== 'undefined' && !arrowPrev.disabled) {
evt.preventDefault();
arrowPrev.click();
}
}
else if (evt.key === 'ArrowRight') {
/* same as clicking the right arrow */
if (typeof arrowNext !== 'undefined' && !arrowNext.disabled) {
evt.preventDefault();
arrowNext.click();
}
}
});
})(); })();
</script> </script>
<button id="arrowPrev" class="nav-arrow nav-arrow--left" aria-label="Previous paragraph" disabled>&larr;</button>
<button id="arrowNext" class="nav-arrow nav-arrow--right" aria-label="Next paragraph" disabled>&rarr;</button>
</body> </body>
</html> </html>