- Add `account_api_key` field to `BotState` and `_adopt_account_api_key()` method that fetches the bot's own `users.api_key` from its profile JSON after auth, switching `LLMClient` to use it instead of the shared bootstrap key, routing gateway spend through the bot's user identity - Replace `interleave_by_author` with a simpler greedy algorithm that picks the first row whose owner differs from the last picked, removing the multi-queue priority logic - Extend `_ai_quota` response with `unlimited`, `requests`, and `last_used` fields sourced from new `user_spend_24h()` analytics helper - Pass `recent_post_titles` from `BotState` into `LLMClient.generate_post()` and add a `distinct_rule` prompt instructing the model to avoid repeating framing, examples, or opinions from the bot's last six posts - Remove early-return dedup check in `ArticleRegistry.admit` that skipped articles already held by the same bot owner, allowing re-admission under a different category - Render bot username as a clickable link with avatar in the admin bots table
94 lines
3.2 KiB
JavaScript
94 lines
3.2 KiB
JavaScript
// retoor <retoor@molodetz.nl>
|
|
|
|
import { OptimisticAction } from "./OptimisticAction.js";
|
|
|
|
export class ReactionBar extends OptimisticAction {
|
|
constructor() {
|
|
super();
|
|
document.addEventListener("click", (event) => this.onClick(event));
|
|
}
|
|
|
|
async onClick(event) {
|
|
const addButton = event.target.closest(".reaction-add-btn");
|
|
if (addButton) {
|
|
event.preventDefault();
|
|
this.togglePalette(addButton);
|
|
return;
|
|
}
|
|
const trigger = event.target.closest("[data-reaction-emoji]");
|
|
if (trigger && !trigger.disabled) {
|
|
event.preventDefault();
|
|
await this.react(trigger);
|
|
return;
|
|
}
|
|
this.closeAllPalettes();
|
|
}
|
|
|
|
togglePalette(addButton) {
|
|
const palette = addButton.parentElement.querySelector(".reaction-palette");
|
|
const isOpen = palette.hidden === false;
|
|
this.closeAllPalettes();
|
|
palette.hidden = isOpen;
|
|
if (!palette.hidden) {
|
|
this.clampToViewport(palette);
|
|
this.ensureInView(palette);
|
|
}
|
|
}
|
|
|
|
clampToViewport(palette) {
|
|
const margin = 8;
|
|
palette.style.left = "";
|
|
const rect = palette.getBoundingClientRect();
|
|
const overflowRight = rect.right - (window.innerWidth - margin);
|
|
if (overflowRight > 0) {
|
|
palette.style.left = `${-overflowRight}px`;
|
|
}
|
|
const adjusted = palette.getBoundingClientRect();
|
|
if (adjusted.left < margin) {
|
|
const current = parseFloat(palette.style.left || "0");
|
|
palette.style.left = `${current + (margin - adjusted.left)}px`;
|
|
}
|
|
}
|
|
|
|
ensureInView(palette) {
|
|
const margin = 8;
|
|
const rect = palette.getBoundingClientRect();
|
|
const extra = rect.height * 0.3;
|
|
if (rect.top < margin) {
|
|
window.scrollBy({ top: rect.top - margin - extra, behavior: "smooth" });
|
|
} else if (rect.bottom > window.innerHeight - margin) {
|
|
window.scrollBy({ top: rect.bottom - (window.innerHeight - margin) + extra, behavior: "smooth" });
|
|
}
|
|
}
|
|
|
|
closeAllPalettes() {
|
|
document.querySelectorAll(".reaction-palette").forEach((palette) => {
|
|
palette.hidden = true;
|
|
});
|
|
}
|
|
|
|
async react(trigger) {
|
|
const bar = trigger.closest(".reaction-bar");
|
|
if (!bar) {
|
|
return;
|
|
}
|
|
const type = bar.dataset.reactionType;
|
|
const uid = bar.dataset.reactionUid;
|
|
const emoji = trigger.dataset.reactionEmoji;
|
|
await this.submit(`/reactions/${type}/${uid}`, { emoji }, null, (result) => this.render(bar, result));
|
|
this.closeAllPalettes();
|
|
}
|
|
|
|
render(bar, result) {
|
|
const counts = result.counts || {};
|
|
const mine = new Set(result.mine || []);
|
|
bar.querySelectorAll(".reaction-list .reaction-chip").forEach((chip) => {
|
|
const emoji = chip.dataset.reactionEmoji;
|
|
const count = counts[emoji] || 0;
|
|
chip.querySelector(".reaction-count").textContent = count;
|
|
chip.hidden = count === 0 && !mine.has(emoji);
|
|
chip.classList.toggle("reacted", mine.has(emoji));
|
|
});
|
|
}
|
|
}
|