Widget script tag
The Ask2Do widget is a self-contained JavaScript bundle that adds a floating chat-bubble button to your admin panel. One <script> tag is the entire install — no npm package, no build-time wiring, no React/Vue/Angular adapters.
At a glance
CDN URL
https://cdn.ask2do.com/widget.js
Auto-updates within ~60s of a new release
Bundle size
~325 KB
Single file. No external CSS to fetch.
Dependencies
None
Self-contained UI, styles, markdown, charts
Role gating
UI + cloud JWT
Non-admins literally can't see the button
Quick start
Paste this immediately before </body> on every page that should expose the assistant:
<script
src="https://cdn.ask2do.com/widget.js"
data-cloud-url="wss://cloud.ask2do.com"
data-panel-role="admin"
defer
></script>For any real integration plug in your own token resolver and user context — see Authentication and Panel user context below.
Versioning & cache behaviour
The CDN serves whatever the latest published release is. Every response carries Cache-Control: public, max-age=60, must-revalidate, so customer browsers revalidate within ~60 seconds. You do not pin a version in the script tag.
Check the running version
- Open DevTools → Console. The widget logs a brand chip:
Ask2Dov0.2.10— the latest release is v0.2.10. window.Ask2Do.versionin the console.curl -s https://cdn.ask2do.com/widget.js | grep -oE '"[0-9]+\.[0-9]+\.[0-9]+"' | head -1
Force a fresh fetch (rarely needed)
Option A — one-time bust. Append a date/build stamp to the script src after a release you want customers to pick up immediately:
<script src="https://cdn.ask2do.com/widget.js?v=YYYYMMDD" ...></script>Bump the value any time you want browsers to re-fetch. The CDN ignores the query string (R2 serves the same object regardless) — it's purely a browser-side cache buster.
Option B — always fresh, on every page load. If your panel is split across multiple subdomains or sub-apps and you've seen them diverge on cached widget versions, drop the static <script> tag entirely and inline this IIFE loader instead. The Date.now() stamp guarantees every page load fetches a fresh copy from the CDN:
<script>
(function () {
var s = document.createElement('script');
s.src = 'https://cdn.ask2do.com/widget.js?t=' + Date.now();
s.defer = true;
s.setAttribute('data-cloud-url', 'wss://cloud.ask2do.com');
s.setAttribute('data-panel-role', 'admin');
document.head.appendChild(s);
})();
</script>Trade-off: every page load re-downloads widget.js (~325 KB) instead of using the browser cache, so first paint is slightly slower and your customer's users pay a bit more bandwidth. Worth it only when you specifically need every panel to converge on the very latest deploy within seconds — otherwise the default max-age=60 revalidation handles it.
Authentication
The widget needs a short-lived JWT signed by cloud.ask2do.com/auth/token. Your panel mints that JWT server-side from your tenant key — never expose the tenant key to the browser. Three integration patterns, pick whichever fits:
1. Endpoint convention (simplest)
<script
src="https://cdn.ask2do.com/widget.js"
data-cloud-url="wss://cloud.ask2do.com"
data-token-endpoint="/api/ask2do/token"
data-panel-role="admin"
defer
></script>Default contract: GET, returns { "token": "<jwt>" }, cookies forwarded via credentials: "include".
2. Custom token resolver (most flexible)
<script>
window.Ask2DoGetToken = async () => {
const r = await fetch("/api/ask2do/token", {
method: "POST",
credentials: "include",
});
if (!r.ok) throw new Error("Ask2Do auth failed");
const { token } = await r.json();
return token;
};
</script>
<script
src="https://cdn.ask2do.com/widget.js"
data-cloud-url="wss://cloud.ask2do.com"
data-panel-role="admin"
defer
></script>3. Imperative mount (advanced)
window.Ask2Do.mount({
cloudUrl: "wss://cloud.ask2do.com",
getToken: async () => (await fetch("/api/ask2do/token")).json().then(r => r.token),
panelUser: { id: "42", role: "admin" },
});Panel user context
The widget shows a button only to users in the admin or owner role. Three ways to declare the current user (priority order):
window.Ask2DoUser = { id, role }set before the widget script (best for SPAs).data-panel-user+data-panel-roleattributes (best for server-rendered HTML).- Just
data-panel-role="admin"— if the entire panel is admin-only (your auth gate enforces it), this silences the pre-login console warning.
Security note: client-side role is a UX hint only. The cloud independently enforces role via the JWT's signed claim — a tampered widget cannot exceed the role baked into its JWT.
Configuration reference
Script-tag data-* attributes
| Attribute | Required | Description |
|---|---|---|
data-cloud-url | Yes | WebSocket origin. Almost always wss://cloud.ask2do.com. |
data-token-endpoint | One of token-endpoint or Ask2DoGetToken | URL the widget fetches with credentials: "include" to mint a JWT. Ignored if window.Ask2DoGetToken is defined. |
data-panel-user | Optional | String id of the signed-in admin. Appears in audit logs. |
data-panel-role | Optional | admin or owner. Other values hide the button. |
defer | Recommended | Lets the widget mount after DOM ready without blocking rendering. |
Window globals
| Global | Direction | Description |
|---|---|---|
window.Ask2DoUser | You set | { id?: string; role: string }. Read once at widget construction. |
window.Ask2DoGetToken | You set | () => Promise<string> — called fresh on every modal open. |
window.Ask2Do.version | Widget sets | String. Useful for support tickets and analytics. |
window.Ask2Do.mount(opts) | Widget sets | Imperative mount API. Returns widget instance with destroy(). |
Built-in features
Everything below ships with the bundle. No flags, no config.
Streaming chat
Replies stream token-by-token; markdown renders progressively.
SQL preview & approval
Writes show a preview card with generated SQL; admin clicks Approve to execute. Read-only queries auto-run.
Chart rendering
Fenced ```chart blocks become live Chart.js canvases. Bar / line / pie / doughnut. PNG download with tenant + date footer.
Table rendering
JSON arrays of rows render as scrollable tables with a CSV download button.
First-visit nudge
On a customer's first ever load, the button slides in with a pulse ring and an auto-shown tooltip. Once per browser (localStorage).
Resizable modal
Users drag the bottom-right corner; preferred size persists in localStorage. 86dvh on desktop, full-viewport on mobile.
Quota awareness
Amber banner at 80% of monthly quota; full block at 100% with an upgrade link.
Reduced-motion respect
Users with prefers-reduced-motion: reduce get a static button, no entrance animation, no pulse.
Customization
Brand colour
.ask2do-button,
.ask2do-overlay {
--ask2do-brand: #6d28d9; /* button bg + accents */
--ask2do-brand-hover: #5b21b6; /* hover state */
--ask2do-brand-50: #f5f3ff; /* light backgrounds */
--ask2do-brand-700: #5b21b6; /* deeper accents */
}Position
Default position is bottom: 24px; right: 24px. Move with regular CSS:
.ask2do-button {
bottom: 16px !important;
right: auto !important;
left: 16px !important; /* flip to bottom-left */
}Framework examples
Pick your stack. Each tab shows a complete copy-paste recipe.
Inject the script via next/script for App Router, or directly in _document.tsx for Pages Router. The async dispatcher pattern lets the script tag run before auth resolves:
<!-- app/layout.tsx (Next.js App Router) -->
<html lang="en">
<body>
{children}
<Script id="ask2do-dispatcher" strategy="beforeInteractive">
{`window.Ask2DoGetToken = async function () {
if (typeof window.__ask2DoFetchToken === 'function') {
return await window.__ask2DoFetchToken();
}
throw new Error('Ask2Do not ready: please sign in first');
};`}
</Script>
<Script
src="https://cdn.ask2do.com/widget.js"
data-cloud-url="wss://cloud.ask2do.com"
data-panel-role="admin"
strategy="afterInteractive"
/>
</body>
</html>Then, in your auth callback (NextAuth, Clerk, Supabase, whatever):
// After successful login:
window.Ask2DoUser = { id: user.id, role: 'admin' };
window.__ask2DoFetchToken = async () => {
const r = await fetch('/api/ask2do/token', { method: 'POST' });
return (await r.json()).token;
};
// On logout, wipe the dispatcher so the widget can't reuse stale auth:
delete window.Ask2DoUser;
delete window.__ask2DoFetchToken;// nuxt.config.ts
export default defineNuxtConfig({
app: {
head: {
script: [
{
innerHTML: `window.Ask2DoGetToken = async function () {
if (typeof window.__ask2DoFetchToken === 'function') {
return await window.__ask2DoFetchToken();
}
throw new Error('Ask2Do not ready');
};`,
tagPosition: 'head',
},
{
src: 'https://cdn.ask2do.com/widget.js',
'data-cloud-url': 'wss://cloud.ask2do.com',
'data-panel-role': 'admin',
defer: true,
tagPosition: 'bodyClose',
},
],
},
},
});<!doctype html>
<html>
<body>
<!-- ...your panel UI... -->
<script>
// Render this from your template engine with the current user.
window.Ask2DoUser = { id: "{{ user.id }}", role: "{{ user.role }}" };
</script>
<script
src="https://cdn.ask2do.com/widget.js"
data-cloud-url="wss://cloud.ask2do.com"
data-token-endpoint="/api/ask2do/token"
defer
></script>
</body>
</html>Need to change the dispatcher or attributes without rebuilding the host app each time? Externalize them into a static file served from your public/ directory and load it dynamically with a per-pageload cache buster. Pattern used in the FTW admin for exactly this reason:
<!-- index.html — inline loader, never needs to change -->
<script>
(function () {
var s = document.createElement('script');
s.src = '/ask2do-bootstrap.js?t=' + Date.now();
document.head.appendChild(s);
})();
</script>// public/ask2do-bootstrap.js — edit + redeploy WITHOUT rebuilding
(function () {
if (window.__ask2DoBootstrapped) return;
window.__ask2DoBootstrapped = true;
window.Ask2DoGetToken = async function () {
if (typeof window.__ask2DoFetchToken === 'function') {
return await window.__ask2DoFetchToken();
}
throw new Error('Ask2Do not ready: please sign in first');
};
var s = document.createElement('script');
s.src = 'https://cdn.ask2do.com/widget.js';
s.defer = true;
s.setAttribute('data-cloud-url', 'wss://cloud.ask2do.com');
s.setAttribute('data-panel-role', 'admin');
document.head.appendChild(s);
})();The bootstrap file is served as a static asset; the per-pageload ?t=Date.now() means browsers fetch it fresh every time. widget.js itself uses the CDN's max-age=60 for auto-propagation. Result: dispatcher tweaks and version updates ship without touching your build artifacts.
Troubleshooting
Button doesn't appear
- Check DevTools Console for the brand chip
Ask2Do v.... No chip ⇒ bundle didn't load (CSP / blocked domains). - Is
window.Ask2DoUser.roleone ofadminorowner? - Has your panel set
display: noneonfixedelements globally? Button usesposition: fixed; z-index: 2147483646.
"Drawing chart…" never resolves
Fixed in v0.2.9 (string-coerced numeric data). If you're still seeing it, hard-refresh once and confirm the running version is ≥ 0.2.9. Open the Console for [ask2do] chart spec rejected — invalid shape: if the AI emitted something genuinely malformed.
Token endpoint returns 401 in production
- Ensure your endpoint forwards the user's session cookie. Widget calls with
credentials: "include"; servers must opt in via CORS for cross-origin endpoints. - Endpoint must call cloud's
/auth/tokenwith your tenant key inAuthorization: Bearer. See backend integration.
Old version hot-loads after a deploy
CDN now sets Cache-Control: public, max-age=60, must-revalidate so this resolves itself within a minute. For the rare emergency — append a one-time ?v=... query param (see Versioning).
Security model
Two independent gates:
- UI gate (advisory) — widget hides the button for non-
admin/ownerroles based on client-sidepanelUser. - JWT gate (authoritative) — cloud checks the signed role claim on every WebSocket message. A tampered widget cannot escalate beyond the role its token endpoint baked in.
Audit log is hash-chained per tenant; see audit log schema. Tenant DB credentials never leave your sidecar (self-hosted) or our NaCl secretbox vault (cloud-hosted) — the widget bundle has zero access to them. Data-protection details →
Next steps
- Backend integration — minting JWTs from your tenant key.
- Install the sidecar — for self-hosted deployments.
- Audit log schema — what every approved write writes.
- Security architecture — the full threat model.