ai-text-popover
Version:
A lightweight, plug-and-play JavaScript utility that allows users to **select text** on a webpage and get a simple explanation for it using Groq's LLMs. Perfect for **glossary-style popovers**, educational tools, or AI-assisted documentation.
157 lines (156 loc) • 6.31 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.initAIPopover = initAIPopover;
function initAIPopover({ apiKey, className = "ai-popover", modelName = "llama-3.3-70b-versatile", // default model
}) {
// for Next JS , React JS frameworks.
if (typeof window === "undefined" || typeof document === "undefined")
return;
const popoverId = "ai-popover";
const numbersOfListtoSave = 10;
// Prevent double injection
if (document.getElementById(popoverId))
return;
// Inject popover styles
const style = document.createElement("style");
if (className !== "ai-popover") {
style.innerHTML = `
.${className} {
position: absolute !important;
z-index: 9999;
display: none;
}
`;
}
else {
style.innerHTML = `
#${popoverId} {
position: absolute;
max-width: 300px;
max-height: 450px;
overflow: auto;
background: white;
color: black;
padding: 10px;
border: 1px solid #ccc;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
z-index: 9999;
display: none;
font-family: inherit;
}
`;
}
document.head.appendChild(style);
// Create popover element
const popover = document.createElement("div");
popover.id = popoverId;
popover.className = className;
document.body.appendChild(popover);
// Cache functions
function getCachedData() {
try {
const data = localStorage.getItem("aiTextPopover");
return data ? JSON.parse(data) : [];
}
catch (_a) {
return [];
}
}
function setDatatoLocalStorage(key, value) {
let cache = getCachedData();
cache = cache.filter((item) => item.key !== key); // Remove duplicates
cache.unshift({ key, value });
if (cache.length > numbersOfListtoSave) {
cache = cache.slice(0, numbersOfListtoSave);
}
localStorage.setItem("aiTextPopover", JSON.stringify(cache));
console.log("Cached:", key);
}
function getDatatoLocalStorage(key) {
const cache = getCachedData();
const found = cache.find((item) => item.key === key);
return found ? found.value : null;
}
// Selection handler
document.addEventListener("mouseup", () => __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c, _d, _e;
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0)
return;
const text = selection.toString().trim();
console.log("Selected text:", text);
if (!text || text.length < 2) {
console.log("Selection too short. Hiding.");
popover.style.display = "none";
return;
}
const range = selection.getRangeAt(0);
const rect = range.getBoundingClientRect();
// Position the popover
popover.style.left = `${rect.left + window.scrollX}px`;
popover.style.top = `${rect.bottom + window.scrollY + 8}px`;
popover.innerText = "Loading...";
console.log("Showing popover");
popover.style.display = "block";
// Check localStorage first
const cached = getDatatoLocalStorage(text);
if (cached) {
console.log("Using cached result");
popover.innerText = cached;
return;
}
// Fetch explanation from API
try {
const res = yield fetch("https://api.groq.com/openai/v1/chat/completions", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: modelName,
messages: [
{
role: "user",
content: `Explain this in simple terms:\n\n"${text}"`,
},
],
}),
});
const data = yield res.json();
let reply = ((_c = (_b = (_a = data === null || data === void 0 ? void 0 : data.choices) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.message) === null || _c === void 0 ? void 0 : _c.content) || "No response from AI.";
// ✅ Handle deprecated model errors gracefully
if ((_e = (_d = data === null || data === void 0 ? void 0 : data.error) === null || _d === void 0 ? void 0 : _d.message) === null || _e === void 0 ? void 0 : _e.includes("model")) {
console.error("Model deprecated or unavailable:", modelName);
reply = `⚠️ The model "${modelName}" is no longer available. Please update to the latest supported model.`;
}
console.log("AI Response:", reply);
popover.innerText = reply;
setDatatoLocalStorage(text, reply);
}
catch (err) {
console.error("Fetch error:", err);
popover.innerText = "Error fetching AI response.";
}
}));
// Hide on input focus
document.addEventListener("focusin", (e) => {
const target = e.target;
if (target.tagName === "INPUT" ||
target.tagName === "TEXTAREA" ||
target.isContentEditable) {
console.log("Hiding popover due to focusin");
popover.style.display = "none";
}
});
}