mirror of
http://88.130.71.182:3000/BlitTech/contexta_be.git
synced 2026-06-12 23:23:21 +00:00
feat: add appointments, campaigns, admin, storage, tests and various updates
- 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>
This commit is contained in:
@@ -1,140 +1,182 @@
|
||||
def generate_widget_js(app_url: str) -> str:
|
||||
"""Generate the embeddable widget JavaScript with app_url baked in."""
|
||||
return f"""
|
||||
(function() {{
|
||||
var APP_URL = "{app_url}";
|
||||
|
||||
// Find script tag to get chatbot ID
|
||||
var scripts = document.querySelectorAll('script[data-chatbot]');
|
||||
var chatbotId = null;
|
||||
if (scripts.length > 0) {{
|
||||
chatbotId = scripts[scripts.length - 1].getAttribute('data-chatbot');
|
||||
}}
|
||||
if (!chatbotId) {{
|
||||
console.warn('[Contexta] No data-chatbot attribute found on script tag');
|
||||
return;
|
||||
}}
|
||||
|
||||
// Styles
|
||||
var style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.contexta-btn {{
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 50%;
|
||||
background: #6366f1;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 999998;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}}
|
||||
.contexta-btn:hover {{
|
||||
transform: scale(1.08);
|
||||
box-shadow: 0 6px 20px rgba(0,0,0,0.25);
|
||||
}}
|
||||
.contexta-btn svg {{
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
fill: white;
|
||||
}}
|
||||
.contexta-container {{
|
||||
position: fixed;
|
||||
bottom: 92px;
|
||||
right: 24px;
|
||||
width: 380px;
|
||||
height: 580px;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.15);
|
||||
z-index: 999999;
|
||||
overflow: hidden;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
border: 1px solid #e5e7eb;
|
||||
}}
|
||||
.contexta-container.open {{
|
||||
display: flex;
|
||||
}}
|
||||
.contexta-iframe {{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
flex: 1;
|
||||
}}
|
||||
.contexta-close {{
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
background: rgba(0,0,0,0.3);
|
||||
border: none;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
}}
|
||||
@media (max-width: 480px) {{
|
||||
.contexta-container {{
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
border-radius: 0;
|
||||
}}
|
||||
}}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
// Button
|
||||
var btn = document.createElement('button');
|
||||
btn.className = 'contexta-btn';
|
||||
btn.setAttribute('aria-label', 'Open chat');
|
||||
btn.innerHTML = '<svg viewBox="0 0 24 24"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z"/></svg>';
|
||||
document.body.appendChild(btn);
|
||||
|
||||
// Container with iframe
|
||||
var container = document.createElement('div');
|
||||
container.className = 'contexta-container';
|
||||
|
||||
var closeBtn = document.createElement('button');
|
||||
closeBtn.className = 'contexta-close';
|
||||
closeBtn.innerHTML = '×';
|
||||
closeBtn.setAttribute('aria-label', 'Close chat');
|
||||
container.appendChild(closeBtn);
|
||||
|
||||
var iframe = document.createElement('iframe');
|
||||
iframe.className = 'contexta-iframe';
|
||||
iframe.src = APP_URL + '/chat/' + chatbotId;
|
||||
iframe.setAttribute('allow', 'microphone');
|
||||
container.appendChild(iframe);
|
||||
|
||||
document.body.appendChild(container);
|
||||
|
||||
// Toggle logic
|
||||
var isOpen = false;
|
||||
function toggle() {{
|
||||
isOpen = !isOpen;
|
||||
if (isOpen) {{
|
||||
container.classList.add('open');
|
||||
btn.innerHTML = '<svg viewBox="0 0 24 24"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>';
|
||||
}} else {{
|
||||
container.classList.remove('open');
|
||||
btn.innerHTML = '<svg viewBox="0 0 24 24"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z"/></svg>';
|
||||
}}
|
||||
}}
|
||||
|
||||
btn.addEventListener('click', toggle);
|
||||
closeBtn.addEventListener('click', toggle);
|
||||
}})();
|
||||
"""
|
||||
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('/'))
|
||||
|
||||
Reference in New Issue
Block a user