Flo widget demo
The launcher should appear bottom-right.
(or inline this file)
* The engine fetches its flow config at runtime, so one engine + one embed
* renders a different conversation per data-flow-id. */
(function () {
var MT = "https://n8n.owlapplicationbuilder.com/webhook/record-crud-mt";
function api(body) {
return fetch(MT, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body)
}).then(function (r) { return r.json(); });
}
function css(theme) {
if (document.getElementById("flo-w-css")) return;
var t = theme || {};
var P = t.primary || "#d85a30", BG = t.panel || "#1a1a1c", TX = t.text || "#f5f5f5";
var s = document.createElement("style");
s.id = "flo-w-css";
s.textContent =
".flo-launch{position:fixed;bottom:22px;right:22px;z-index:99999;display:flex;align-items:center;gap:10px;" +
"background:" + P + ";color:#fff;border:none;border-radius:9999px;padding:12px 18px;font:600 15px/1 Inter,system-ui,sans-serif;cursor:pointer;box-shadow:0 6px 24px rgba(0,0,0,.3)}" +
".flo-launch img{width:28px;height:28px;border-radius:50%;object-fit:cover}" +
".flo-panel{position:fixed;bottom:22px;right:22px;z-index:100000;width:360px;max-width:calc(100vw - 32px);height:520px;max-height:calc(100vh - 44px);" +
"background:" + BG + ";color:" + TX + ";border-radius:16px;display:none;flex-direction:column;overflow:hidden;font:14px/1.5 Inter,system-ui,sans-serif;box-shadow:0 12px 48px rgba(0,0,0,.45)}" +
".flo-panel.open{display:flex}" +
".flo-hd{display:flex;align-items:center;gap:10px;padding:14px 16px;background:rgba(255,255,255,.05);border-bottom:1px solid rgba(255,255,255,.08)}" +
".flo-hd img{width:36px;height:36px;border-radius:50%;object-fit:cover}" +
".flo-hd b{font-size:15px}.flo-hd span{font-size:12px;opacity:.6;display:block}" +
".flo-x{margin-left:auto;background:none;border:none;color:" + TX + ";font-size:20px;cursor:pointer;opacity:.6}" +
".flo-body{flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:10px}" +
".flo-msg{max-width:80%;padding:10px 14px;border-radius:14px}" +
".flo-bot{align-self:flex-start;background:rgba(255,255,255,.08)}" +
".flo-user{align-self:flex-end;background:" + P + ";color:#fff}" +
".flo-opts{display:flex;flex-wrap:wrap;gap:8px;padding:0 16px 14px}" +
".flo-opt{background:transparent;border:1px solid " + P + ";color:" + TX + ";border-radius:9999px;padding:8px 14px;font:500 13px Inter,system-ui,sans-serif;cursor:pointer}" +
".flo-opt:hover{background:" + P + ";color:#fff}" +
".flo-inrow{display:flex;gap:8px;padding:0 16px 16px}" +
".flo-in{flex:1;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.12);color:" + TX + ";border-radius:10px;padding:10px 12px;font:14px Inter,system-ui,sans-serif}" +
".flo-send{background:" + P + ";color:#fff;border:none;border-radius:10px;padding:0 16px;font:600 14px Inter;cursor:pointer}";
document.head.appendChild(s);
}
function Widget(el, flow) {
var agent = flow.agent || {}, answers = {}, body, opts, panel;
css(flow.theme);
var launch = document.createElement("button");
launch.className = "flo-launch";
launch.innerHTML = (agent.avatar ? '

' : "") +
"
" + (flow.launcher || "Chat with us") + "";
panel = document.createElement("div");
panel.className = "flo-panel";
panel.innerHTML =
'
' + (agent.avatar ? '

' : "") +
"
" + (agent.name || "Assistant") + "" + (agent.tagline || "Typically replies instantly") + "
" +
'
' +
'
';
body = panel.querySelector(".flo-body");
opts = panel.querySelector(".flo-opts");
function open() { panel.classList.add("open"); launch.style.display = "none"; }
function close() { panel.classList.remove("open"); launch.style.display = "flex"; }
launch.onclick = function () { open(); if (!body.children.length) go(flow.start); };
panel.querySelector(".flo-x").onclick = close;
function scroll() { body.scrollTop = body.scrollHeight; }
function bot(txt) { var d = document.createElement("div"); d.className = "flo-msg flo-bot"; d.textContent = txt; body.appendChild(d); scroll(); }
function user(txt) { var d = document.createElement("div"); d.className = "flo-msg flo-user"; d.textContent = txt; body.appendChild(d); scroll(); }
function clearOpts() { opts.innerHTML = ""; }
function go(id) {
var step = (flow.steps || {})[id];
clearOpts();
if (!step) return;
(step.bot || []).forEach(bot);
if (step.capture) { capture(); return; }
if (step.choices) {
step.choices.forEach(function (c) {
var b = document.createElement("button");
b.className = "flo-opt"; b.textContent = c.label;
b.onclick = function () { user(c.label); if (c.field) answers[c.field] = c.label; clearOpts(); go(c.next); };
opts.appendChild(b);
});
} else if (step.input) {
var row = document.createElement("div"); row.className = "flo-inrow";
var inp = document.createElement("input"); inp.className = "flo-in";
inp.placeholder = step.input.placeholder || ""; inp.type = step.input.type || "text";
var snd = document.createElement("button"); snd.className = "flo-send"; snd.textContent = "Send";
function submit() {
var v = inp.value.trim(); if (!v) return;
answers[step.input.field] = v; user(v); opts.innerHTML = ""; go(step.next);
}
snd.onclick = submit; inp.onkeydown = function (e) { if (e.key === "Enter") submit(); };
row.appendChild(inp); row.appendChild(snd); opts.appendChild(row); inp.focus();
}
}
function capture() {
var lead = flow.capture || {};
var rec = Object.assign({ source: "flo_widget", flow_id: flow.flow_id, captured_at: Date.now() }, answers);
if (lead.object) {
api(Object.assign({ action: "create", object_name: lead.object, app: flow.app }, rec))
.catch(function () {});
}
}
el.appendChild(launch);
el.appendChild(panel);
if (flow.autoOpen) launch.click();
}
function start(el) {
var flowId = el.getAttribute("data-flow-id");
if (!flowId || el.getAttribute("data-flo-ready")) return;
el.setAttribute("data-flo-ready", "1");
var app = el.getAttribute("data-app");
var obj = el.getAttribute("data-flow-object") || "pages";
var key = el.getAttribute("data-flow-key") || "slug";
var filter = {}; filter[key] = flowId;
api({ action: "get_v2", app: app, object_name: obj,
filter: filter, fields: ["flow", "content", "contents"], page_size: 1 })
.then(function (res) {
var rec = ((res ? res.data : null) || [])[0] || {};
var raw = rec.flow || rec.content || rec.contents || null;
var flow = (typeof raw === "string") ? JSON.parse(raw) : raw;
if (flow) { flow.app = flow.app || app; new Widget(el, flow); }
})
.catch(function (e) { console.error("flo-widget load failed", e); });
}
function init() { [].forEach.call(document.querySelectorAll("[data-flo-widget]"), start); }
if (document.readyState !== "loading") init();
else document.addEventListener("DOMContentLoaded", init);
})();