antern-bot / static /embed-inline.html
johnpitteera's picture
Upload folder using huggingface_hub
2dd2de0 verified
Raw
History Blame Contribute Delete
15.2 kB
<!-- ======================================================================
ANTERN BOT — embeddable chat widget (inline build for ngrok free tier)
HOW TO USE:
Copy the entire <script>...</script> block below and paste it just
before the closing </body> tag on any page of your website.
Why inline? ngrok's free tier shows a browser-warning page on a normal
<script src="...ngrok...">, so the widget code is inlined here instead.
The only call that touches ngrok is the chat API request, which sends the
"ngrok-skip-browser-warning" header and works fine. (Chart.js, when a chart
is shown, loads from the jsDelivr CDN — not from ngrok.)
To change the bot URL, title, colour, or greeting, edit the four CONFIG
lines at the top of the script.
====================================================================== -->
<script>
(function () {
/* ---- CONFIG (edit these) ---- */
var API = "https://duckling-bovine-blitz.ngrok-free.dev"; // your stable bot URL
var TITLE = "Antern Bot";
var ACCENT = "#4f8cff";
var GREETING = "👋 Hi! I'm Antern Bot, your data assistant.\n\nAsk me anything about your recruitment data — candidates, requirements, applications, interviews — in plain English.\n\nTry: “How many active candidates do we have?”";
/* ----------------------------- */
var STORAGE_KEY = "antern_bot_session";
API = API.replace(/\/$/, "");
var host = document.createElement("div");
host.id = "antern-bot-widget";
document.body.appendChild(host);
var root = host.attachShadow({ mode: "open" });
root.innerHTML =
'<style>' +
':host{all:initial;}' +
'*{box-sizing:border-box;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;}' +
'.launcher{position:fixed;bottom:24px;right:24px;width:62px;height:62px;border-radius:50%;border:none;cursor:pointer;background:linear-gradient(135deg,' + ACCENT + ',#8a5cff);color:#fff;z-index:2147483000;display:flex;align-items:center;justify-content:center;box-shadow:0 10px 28px rgba(0,0,0,.30),0 4px 12px rgba(79,140,255,.45);transition:transform .25s cubic-bezier(.2,.9,.3,1.2),box-shadow .25s;animation:antern-pop .5s cubic-bezier(.2,.9,.3,1.2);}' +
'.launcher::after{content:"";position:absolute;inset:0;border-radius:50%;background:linear-gradient(135deg,' + ACCENT + ',#8a5cff);z-index:-1;animation:antern-pulse 2.6s ease-out infinite;}' +
'.launcher.open::after{animation:none;opacity:0;}' +
'.launcher:hover{transform:translateY(-3px) scale(1.05);box-shadow:0 16px 38px rgba(0,0,0,.34),0 6px 18px rgba(79,140,255,.55);}' +
'.launcher:active{transform:translateY(-1px) scale(.97);}' +
'.launcher svg{width:27px;height:27px;}' +
'.launcher .ic-close{display:none;}' +
'.launcher.open .ic-chat{display:none;}' +
'.launcher.open .ic-close{display:block;}' +
'@keyframes antern-pop{0%{transform:scale(0) rotate(-25deg);}100%{transform:scale(1) rotate(0);}}' +
'@keyframes antern-pulse{0%{transform:scale(1);opacity:.45;}70%{transform:scale(1.7);opacity:0;}100%{opacity:0;}}' +
'.panel{position:fixed;bottom:92px;right:22px;width:760px;max-width:calc(100vw - 32px);height:1120px;max-height:calc(100vh - 120px);background:#0f1115;border:1px solid #2a303c;border-radius:16px;box-shadow:0 12px 40px rgba(0,0,0,.4);z-index:2147483000;display:none;flex-direction:column;overflow:hidden;}' +
'.panel.open{display:flex;}' +
'.head{background:#171a21;border-bottom:1px solid #2a303c;padding:13px 16px;display:flex;align-items:center;gap:10px;color:#e8eaed;}' +
'.head .badge{width:28px;height:28px;border-radius:8px;background:' + ACCENT + ';display:flex;align-items:center;justify-content:center;font-weight:700;}' +
'.head h3{margin:0;font-size:15px;flex:1;}' +
'.head button{background:none;border:none;color:#9aa3b2;cursor:pointer;font-size:20px;line-height:1;}' +
'.body{flex:1;overflow-y:auto;padding:16px;background:#0f1115;}' +
'.msg{display:flex;gap:9px;margin-bottom:14px;}' +
'.msg .av{width:26px;height:26px;border-radius:7px;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;color:#fff;}' +
'.msg.bot .av{background:' + ACCENT + ';}' +
'.msg.user .av{background:#374151;}' +
'.bubble{background:#232936;border:1px solid #2a303c;border-radius:11px;padding:10px 13px;color:#e8eaed;font-size:14px;line-height:1.5;white-space:pre-wrap;word-wrap:break-word;max-width:100%;}' +
'.msg.user .bubble{background:#1f242e;}' +
'.typing{color:#9aa3b2;font-style:italic;}' +
'.foot{border-top:1px solid #2a303c;background:#171a21;padding:11px;display:flex;gap:8px;}' +
'.foot textarea{flex:1;resize:none;background:#1f242e;color:#e8eaed;border:1px solid #2a303c;border-radius:9px;padding:9px 11px;font-size:14px;max-height:120px;}' +
'.foot textarea:focus{outline:none;border-color:' + ACCENT + ';}' +
'.foot button{background:' + ACCENT + ';color:#fff;border:none;border-radius:9px;padding:0 16px;font-weight:600;cursor:pointer;font-size:14px;}' +
'.foot button:disabled{opacity:.5;cursor:not-allowed;}' +
'.cap{font-size:11.5px;color:#9aa3b2;margin:10px 0 4px;}' +
'.tbl-wrap{margin-top:8px;max-height:260px;overflow:auto;border:1px solid #2a303c;border-radius:9px;}' +
'table.tbl{border-collapse:collapse;width:100%;font-size:12.5px;}' +
'table.tbl th,table.tbl td{padding:6px 9px;border-bottom:1px solid #2a303c;text-align:left;white-space:nowrap;}' +
'table.tbl th{position:sticky;top:0;background:#1f242e;color:#cbd5e1;font-weight:600;}' +
'table.tbl td{color:#e8eaed;}' +
'table.tbl tr:hover td{background:#1a1f29;}' +
'.chart-wrap{margin-top:8px;background:#fff;border-radius:9px;padding:10px;}' +
'.chart-wrap canvas{max-width:100%;}' +
'.stats{padding:7px 12px;background:#12151b;border-top:1px solid #2a303c;color:#9aa3b2;font-size:11px;text-align:center;}' +
'</style>' +
'<button class="launcher" aria-label="Open chat">' +
'<svg class="ic-chat" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 11.5a8.5 8.5 0 0 1-12.5 7.48L3 21l2.02-5.5A8.5 8.5 0 1 1 21 11.5z"/><circle cx="8.5" cy="11.5" r="1" fill="currentColor" stroke="none"/><circle cx="12" cy="11.5" r="1" fill="currentColor" stroke="none"/><circle cx="15.5" cy="11.5" r="1" fill="currentColor" stroke="none"/></svg>' +
'<svg class="ic-close" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9l6 6 6-6"/></svg>' +
'</button>' +
'<div class="panel" role="dialog" aria-label="' + TITLE + '">' +
' <div class="head"><div class="badge">A</div><h3>' + TITLE + '</h3><button class="close" aria-label="Close">×</button></div>' +
' <div class="body"></div>' +
' <div class="foot"><textarea rows="1" placeholder="Type your question…"></textarea><button class="send">Send</button></div>' +
' <div class="stats"></div>' +
'</div>';
var launcher = root.querySelector(".launcher");
var panel = root.querySelector(".panel");
var closeBtn = root.querySelector(".close");
var body = root.querySelector(".body");
var input = root.querySelector("textarea");
var sendBtn = root.querySelector(".send");
var statsEl = root.querySelector(".stats");
var sessionId = localStorage.getItem(STORAGE_KEY) || null;
var greeted = false;
var PALETTE = ["#4f8cff","#8a5cff","#38d39f","#ffb020","#ff6b6b","#22b8cf","#f783ac","#a0d911"];
var chartJsPromise = null;
function loadChartJs() {
if (window.Chart) return Promise.resolve(window.Chart);
if (chartJsPromise) return chartJsPromise;
chartJsPromise = new Promise(function (resolve, reject) {
var s = document.createElement("script");
s.src = "https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js";
s.onload = function () { resolve(window.Chart); };
s.onerror = function () { reject(new Error("chart lib failed")); };
document.head.appendChild(s);
});
return chartJsPromise;
}
function renderTable(container, p) {
var cols = p.columns || [], rows = p.rows || [];
if (p.title) { var c = document.createElement("div"); c.className = "cap"; c.textContent = p.title; container.appendChild(c); }
var wrap = document.createElement("div"); wrap.className = "tbl-wrap";
var t = document.createElement("table"); t.className = "tbl";
var thead = document.createElement("thead"), trh = document.createElement("tr");
cols.forEach(function (col) { var th = document.createElement("th"); th.textContent = col; trh.appendChild(th); });
thead.appendChild(trh); t.appendChild(thead);
var tb = document.createElement("tbody");
rows.forEach(function (r) {
var tr = document.createElement("tr");
r.forEach(function (v) { var td = document.createElement("td"); td.textContent = (v === null || v === undefined) ? "" : v; tr.appendChild(td); });
tb.appendChild(tr);
});
t.appendChild(tb); wrap.appendChild(t); container.appendChild(wrap);
if (p.truncated) { var n = document.createElement("div"); n.className = "cap"; n.textContent = "(showing first " + rows.length + " rows)"; container.appendChild(n); }
}
function renderChart(container, p) {
var cols = p.columns || [], rows = p.rows || [];
var xi = cols.indexOf(p.x_field);
var yIdx = (p.y_fields || []).map(function (f) { return cols.indexOf(f); }).filter(function (i) { return i >= 0; });
if (xi < 0 || yIdx.length === 0) { renderTable(container, p); return; }
if (p.title) { var cap = document.createElement("div"); cap.className = "cap"; cap.textContent = p.title; container.appendChild(cap); }
var wrap = document.createElement("div"); wrap.className = "chart-wrap";
var canvas = document.createElement("canvas"); wrap.appendChild(canvas); container.appendChild(wrap);
var labels = rows.map(function (r) { return r[xi]; });
loadChartJs().then(function (Chart) {
if (p.format === "pie") {
var fi = yIdx[0];
new Chart(canvas, {
type: "pie",
data: { labels: labels, datasets: [{ data: rows.map(function (r) { return Number(r[fi]) || 0; }), backgroundColor: labels.map(function (_, i) { return PALETTE[i % PALETTE.length]; }) }] },
options: { responsive: true, plugins: { legend: { position: "right" } } },
});
} else {
var datasets = yIdx.map(function (ci, k) {
return { label: cols[ci], data: rows.map(function (r) { return Number(r[ci]) || 0; }), backgroundColor: PALETTE[k % PALETTE.length], borderColor: PALETTE[k % PALETTE.length], fill: false, tension: 0.25 };
});
new Chart(canvas, {
type: p.format === "line" ? "line" : "bar",
data: { labels: labels, datasets: datasets },
options: { responsive: true, plugins: { legend: { display: yIdx.length > 1 } }, scales: { y: { beginAtZero: true } } },
});
}
body.scrollTop = body.scrollHeight;
}).catch(function () {
var e = document.createElement("div"); e.className = "cap"; e.textContent = "(couldn't load chart — showing table)"; container.appendChild(e);
renderTable(container, p);
});
}
function renderPresentation(bubble, p) {
if (!p || !p.format || p.format === "text") return;
var box = document.createElement("div");
bubble.appendChild(box);
if (p.format === "table") renderTable(box, p);
else renderChart(box, p);
}
var STATS_KEY = "antern_bot_daystats";
var lastTokens = null;
function _today() { return new Date().toISOString().slice(0, 10); }
function getStats() {
var s;
try { s = JSON.parse(localStorage.getItem(STATS_KEY) || "{}"); } catch (e) { s = {}; }
if (s.date !== _today()) s = { date: _today(), chats: 0, cost: 0 };
return s;
}
function bumpStats(cost) {
var s = getStats();
s.chats += 1; s.cost += (cost || 0);
localStorage.setItem(STATS_KEY, JSON.stringify(s));
}
function renderStats() {
var s = getStats();
var avg = s.chats ? s.cost / s.chats : 0;
var parts = ["Today: " + s.chats + " chat" + (s.chats === 1 ? "" : "s"),
"avg $" + avg.toFixed(4) + "/chat"];
if (lastTokens != null) parts.push("last: " + lastTokens.toLocaleString() + " tokens");
statsEl.textContent = parts.join(" · ");
}
function esc(s) {
return (s == null ? "" : String(s)).replace(/[&<>"]/g, function (c) {
return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c];
});
}
function addMsg(role, text) {
var m = document.createElement("div");
m.className = "msg " + role;
m.innerHTML =
'<div class="av">' + (role === "user" ? "You" : "A") + "</div>" +
'<div class="bubble"></div>';
m.querySelector(".bubble").textContent = text;
body.appendChild(m);
body.scrollTop = body.scrollHeight;
return m;
}
function setOpen(open) {
panel.classList.toggle("open", open);
launcher.classList.toggle("open", open);
launcher.setAttribute("aria-label", open ? "Close chat" : "Open chat");
if (open && !greeted) { addMsg("bot", GREETING); greeted = true; }
if (open) { renderStats(); input.focus(); }
}
async function send(text) {
addMsg("user", text);
sendBtn.disabled = true;
var m = addMsg("bot", "");
var bubble = m.querySelector(".bubble");
bubble.innerHTML = '<span class="typing">Thinking…</span>';
try {
var r = await fetch(API + "/api/chat", {
method: "POST",
headers: {
"Content-Type": "application/json",
"ngrok-skip-browser-warning": "true",
},
body: JSON.stringify({ message: text, session_id: sessionId }),
});
var d = await r.json();
if (!r.ok) {
bubble.innerHTML = '<span class="typing">⚠ ' + esc(d.error || "Error") + "</span>";
} else {
sessionId = d.session_id;
localStorage.setItem(STORAGE_KEY, sessionId);
bubble.textContent = d.answer || "(no answer)";
renderPresentation(bubble, d.presentation);
if (typeof d.cost_usd === "number") bumpStats(d.cost_usd);
if (typeof d.tokens === "number") lastTokens = d.tokens;
renderStats();
}
} catch (e) {
bubble.innerHTML = '<span class="typing">⚠ Network error</span>';
} finally {
sendBtn.disabled = false;
body.scrollTop = body.scrollHeight;
}
}
function submit() {
var t = input.value.trim();
if (!t) return;
input.value = "";
input.style.height = "auto";
send(t);
}
launcher.addEventListener("click", function () { setOpen(!panel.classList.contains("open")); });
closeBtn.addEventListener("click", function () { setOpen(false); });
sendBtn.addEventListener("click", submit);
input.addEventListener("keydown", function (e) {
if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); submit(); }
});
input.addEventListener("input", function () {
input.style.height = "auto";
input.style.height = Math.min(input.scrollHeight, 120) + "px";
});
renderStats();
})();
</script>