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); })();