ReplyFlow / app /static /widget /replyflow-widget.js
nvikou's picture
Initial commit
582520a verified
Raw History Blame Contribute Delete
4.58 kB
/**
* ReplyFlow embeddable widget.
* Usage: <script src="https://YOUR_HOST/widget.js" defer></script>
*/
(function () {
if (window.__ReplyFlowWidgetLoaded) return;
window.__ReplyFlowWidgetLoaded = true;
const script =
document.currentScript ||
document.querySelector('script[src*="widget.js"]');
const base =
(script && script.src
? script.src.replace(/\/widget\.js(?:\?.*)?$/, "")
: window.location.origin) || "";
const style = document.createElement("style");
style.textContent = `
#rf-launcher{position:fixed;right:20px;bottom:20px;z-index:99999;
width:58px;height:58px;border-radius:50%;border:0;cursor:pointer;
background:linear-gradient(135deg,#d4a24c,#e8c078);color:#1a1408;
font:700 14px/1 Manrope,system-ui,sans-serif;box-shadow:0 12px 30px rgba(0,0,0,.35)}
#rf-panel{position:fixed;right:20px;bottom:90px;z-index:99999;width:min(360px,calc(100vw - 24px));
height:min(520px,70vh);display:none;flex-direction:column;overflow:hidden;
border-radius:18px;background:#16302b;color:#f3f0e8;border:1px solid rgba(243,240,232,.12);
box-shadow:0 24px 60px rgba(0,0,0,.4);font:14px/1.4 Manrope,system-ui,sans-serif}
#rf-panel.open{display:flex}
#rf-head{padding:12px 14px;border-bottom:1px solid rgba(243,240,232,.12)}
#rf-head strong{font-family:Syne,Manrope,sans-serif;letter-spacing:-.02em}
#rf-msgs{flex:1;overflow:auto;padding:12px;display:flex;flex-direction:column;gap:8px}
.rf-b{max-width:88%;padding:8px 10px;border-radius:12px;white-space:pre-wrap}
.rf-u{align-self:flex-end;background:rgba(212,162,76,.22)}
.rf-a{align-self:flex-start;background:rgba(255,255,255,.06);border:1px solid rgba(243,240,232,.1)}
#rf-form{display:flex;gap:6px;padding:10px;border-top:1px solid rgba(243,240,232,.12)}
#rf-form input{flex:1;border-radius:999px;border:1px solid rgba(243,240,232,.15);
background:rgba(0,0,0,.25);color:#f3f0e8;padding:10px 12px;font:inherit}
#rf-form button{border:0;border-radius:999px;padding:10px 12px;cursor:pointer;
background:linear-gradient(135deg,#d4a24c,#e8c078);color:#1a1408;font-weight:700}
`;
document.head.appendChild(style);
const launcher = document.createElement("button");
launcher.id = "rf-launcher";
launcher.type = "button";
launcher.setAttribute("aria-label", "Open ReplyFlow chat");
launcher.textContent = "RF";
const panel = document.createElement("div");
panel.id = "rf-panel";
panel.innerHTML = `
<div id="rf-head">
<strong>ReplyFlow</strong>
</div>
<div id="rf-msgs"></div>
<form id="rf-form">
<input id="rf-input" placeholder="Message…" autocomplete="off" />
<button type="submit">Send</button>
</form>
`;
document.body.appendChild(panel);
document.body.appendChild(launcher);
const msgs = panel.querySelector("#rf-msgs");
const form = panel.querySelector("#rf-form");
const input = panel.querySelector("#rf-input");
let conversationId = null;
let started = false;
function bubble(role, text) {
const el = document.createElement("div");
el.className = `rf-b ${role === "user" ? "rf-u" : "rf-a"}`;
el.textContent = text;
msgs.appendChild(el);
msgs.scrollTop = msgs.scrollHeight;
}
async function start() {
msgs.innerHTML = "";
const res = await fetch(
`${base}/api/chat/start?ui_language=en`,
{ method: "POST" }
);
const data = await res.json();
conversationId = data.conversation_id;
bubble("assistant", data.greeting);
started = true;
}
launcher.addEventListener("click", async () => {
panel.classList.toggle("open");
if (panel.classList.contains("open") && !started) {
await start();
}
});
form.addEventListener("submit", async (e) => {
e.preventDefault();
const text = input.value.trim();
if (!text || !conversationId) return;
input.value = "";
bubble("user", text);
const res = await fetch(`${base}/api/chat/message`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
conversation_id: conversationId,
message: text,
ui_language: "en",
}),
});
const data = await res.json();
bubble("assistant", data.reply || "…");
});
})();