mirror of
http://88.130.71.182:3000/BlitTech/contexta_be.git
synced 2026-06-13 10:14:58 +00:00
- Add new routers: admin, appointments, campaigns - Add storage service and logging config - Add migrations directory and test suite with pytest config - Add supabase_migration_features.sql - Update models, dependencies, config, and existing routers - Remove whatsapp_service (deleted) - Update pyproject.toml and uv.lock dependencies Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
183 lines
7.4 KiB
Python
183 lines
7.4 KiB
Python
"""
|
|
Widget JS generator.
|
|
|
|
Produces a self-contained, framework-agnostic JavaScript bundle served at
|
|
GET /widget.js. Embed on any page with:
|
|
|
|
<script
|
|
src="https://api.yoursite.com/widget.js"
|
|
data-chatbot="<chatbot-id>">
|
|
</script>
|
|
|
|
Works on vanilla HTML, WordPress, Webflow, Shopify, Next.js (_document),
|
|
and any framework where you control the HTML shell.
|
|
|
|
For React/Vue projects that want a native component, host-side devs can call
|
|
window.Contexta.open() / .close() / .toggle()
|
|
from their own button, or await the dedicated npm package (@contexta/widget).
|
|
|
|
Design decisions
|
|
----------------
|
|
- All CSS is ID-scoped (#ctxa-*) to avoid colliding with host-page styles.
|
|
- The iframe src is set lazily on first open — zero network cost until use.
|
|
- document.currentScript is captured synchronously (before any async code)
|
|
so it works even when the host page has many script tags.
|
|
- z-index 2147483647 is the highest a browser will honour.
|
|
- sandbox attribute restricts the iframe while still allowing forms/popups.
|
|
"""
|
|
|
|
_TEMPLATE = r"""(function () {
|
|
'use strict';
|
|
|
|
/* ── Double-init guard ─────────────────────────────────────────────── */
|
|
if (window.__ctxa) return;
|
|
|
|
/* ── Read chatbot ID from <script data-chatbot="..."> ──────────────── */
|
|
var _cur = document.currentScript;
|
|
var _id = _cur && _cur.getAttribute('data-chatbot');
|
|
if (!_id) {
|
|
var _all = document.querySelectorAll('script[data-chatbot]');
|
|
if (_all.length) _id = _all[_all.length - 1].getAttribute('data-chatbot');
|
|
}
|
|
if (!_id) {
|
|
console.warn('[Contexta] widget loaded but no data-chatbot attribute found.');
|
|
return;
|
|
}
|
|
|
|
var _base = '__APP_URL__';
|
|
var _url = _base + '/chat/' + _id;
|
|
|
|
/* ── Styles ─────────────────────────────────────────────────────────── */
|
|
var _css = [
|
|
'#ctxa-btn{',
|
|
'position:fixed;bottom:24px;right:24px;',
|
|
'width:56px;height:56px;border-radius:50%;',
|
|
'background:linear-gradient(135deg,#6366f1,#4f46e5);',
|
|
'border:none;cursor:pointer;padding:0;',
|
|
'box-shadow:0 4px 20px rgba(99,102,241,.45);',
|
|
'display:flex;align-items:center;justify-content:center;',
|
|
'z-index:2147483646;',
|
|
'transition:transform .2s ease,box-shadow .2s ease;',
|
|
'outline:none;',
|
|
'}',
|
|
'#ctxa-btn:hover{transform:scale(1.1);box-shadow:0 6px 28px rgba(99,102,241,.55)}',
|
|
'#ctxa-btn:focus-visible{outline:2px solid #6366f1;outline-offset:3px}',
|
|
|
|
/* Icon animations */
|
|
'#ctxa-ico-chat,#ctxa-ico-x{position:absolute;transition:opacity .15s ease,transform .2s ease}',
|
|
'#ctxa-ico-x{opacity:0;transform:rotate(-90deg)}',
|
|
'#ctxa-btn.open #ctxa-ico-chat{opacity:0;transform:rotate(90deg)}',
|
|
'#ctxa-btn.open #ctxa-ico-x{opacity:1;transform:rotate(0)}',
|
|
|
|
/* Panel */
|
|
'#ctxa-panel{',
|
|
'position:fixed;bottom:92px;right:24px;',
|
|
'width:380px;height:600px;',
|
|
'max-height:calc(100dvh - 120px);',
|
|
'border-radius:20px;overflow:hidden;background:#fff;',
|
|
'box-shadow:0 20px 60px rgba(0,0,0,.18),0 0 0 1px rgba(0,0,0,.07);',
|
|
'z-index:2147483647;',
|
|
'opacity:0;transform:translateY(12px) scale(.97);pointer-events:none;',
|
|
'transition:opacity .22s cubic-bezier(.4,0,.2,1),transform .22s cubic-bezier(.4,0,.2,1);',
|
|
'}',
|
|
'#ctxa-panel.open{opacity:1;transform:translateY(0) scale(1);pointer-events:all}',
|
|
'#ctxa-frame{width:100%;height:100%;border:none;display:block;background:#fff}',
|
|
|
|
/* Mobile: full-screen */
|
|
'@media(max-width:480px){',
|
|
'#ctxa-panel{bottom:0;right:0;left:0;width:100%;height:100%;',
|
|
'max-height:100dvh;border-radius:0;box-shadow:none}',
|
|
'#ctxa-btn{bottom:16px;right:16px}',
|
|
'}',
|
|
].join('');
|
|
|
|
var _style = document.createElement('style');
|
|
_style.textContent = _css;
|
|
document.head.appendChild(_style);
|
|
|
|
/* ── Button ─────────────────────────────────────────────────────────── */
|
|
var _btn = document.createElement('button');
|
|
_btn.id = 'ctxa-btn';
|
|
_btn.setAttribute('aria-label', 'Open chat');
|
|
_btn.setAttribute('aria-expanded', 'false');
|
|
_btn.innerHTML = (
|
|
'<svg id="ctxa-ico-chat" width="24" height="24" viewBox="0 0 24 24" fill="none">' +
|
|
'<path d="M12 2C6.477 2 2 6.2 2 11.4c0 2.8 1.26 5.3 3.26 7.04L4 22l4.2-1.75' +
|
|
'A11.1 11.1 0 0 0 12 20.8c5.523 0 10-4.2 10-9.4S17.523 2 12 2z" fill="white"/>' +
|
|
'</svg>' +
|
|
'<svg id="ctxa-ico-x" width="20" height="20" viewBox="0 0 24 24" fill="none">' +
|
|
'<path d="M18 6L6 18M6 6l12 12" stroke="white" stroke-width="2.5"' +
|
|
' stroke-linecap="round" stroke-linejoin="round"/>' +
|
|
'</svg>'
|
|
);
|
|
|
|
/* ── Panel + lazy iframe ─────────────────────────────────────────────── */
|
|
var _panel = document.createElement('div');
|
|
_panel.id = 'ctxa-panel';
|
|
_panel.setAttribute('role', 'dialog');
|
|
_panel.setAttribute('aria-label', 'Chat');
|
|
|
|
var _frame = document.createElement('iframe');
|
|
_frame.id = 'ctxa-frame';
|
|
_frame.title = 'Contexta chat';
|
|
_frame.setAttribute('allow', 'clipboard-write');
|
|
/* sandbox: scripts + same-origin needed for the React app to run */
|
|
_frame.setAttribute('sandbox',
|
|
'allow-scripts allow-same-origin allow-forms allow-popups allow-top-navigation-by-user-activation'
|
|
);
|
|
_panel.appendChild(_frame);
|
|
|
|
/* ── Mount after DOM ready ───────────────────────────────────────────── */
|
|
function _mount() {
|
|
document.body.appendChild(_btn);
|
|
document.body.appendChild(_panel);
|
|
}
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', _mount);
|
|
} else {
|
|
_mount();
|
|
}
|
|
|
|
/* ── Open / Close ────────────────────────────────────────────────────── */
|
|
var _isOpen = false;
|
|
var _loaded = false;
|
|
|
|
function _open() {
|
|
if (!_loaded) { _frame.src = _url; _loaded = true; }
|
|
_isOpen = true;
|
|
_panel.classList.add('open');
|
|
_btn.classList.add('open');
|
|
_btn.setAttribute('aria-expanded', 'true');
|
|
_btn.setAttribute('aria-label', 'Close chat');
|
|
}
|
|
|
|
function _close() {
|
|
_isOpen = false;
|
|
_panel.classList.remove('open');
|
|
_btn.classList.remove('open');
|
|
_btn.setAttribute('aria-expanded', 'false');
|
|
_btn.setAttribute('aria-label', 'Open chat');
|
|
}
|
|
|
|
_btn.addEventListener('click', function () { _isOpen ? _close() : _open(); });
|
|
|
|
document.addEventListener('keydown', function (e) {
|
|
if (e.key === 'Escape' && _isOpen) _close();
|
|
});
|
|
|
|
/* ── Public API ──────────────────────────────────────────────────────── */
|
|
window.__ctxa = true;
|
|
window.Contexta = {
|
|
open: _open,
|
|
close: _close,
|
|
toggle: function () { _isOpen ? _close() : _open(); },
|
|
};
|
|
|
|
}());
|
|
"""
|
|
|
|
|
|
def generate_widget_js(app_url: str) -> str:
|
|
"""Return the widget bundle with the frontend app URL baked in."""
|
|
return _TEMPLATE.replace('__APP_URL__', app_url.rstrip('/'))
|