Ask2DoAsk2Do

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:

html
<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

  1. Open DevTools → Console. The widget logs a brand chip: Ask2Dov0.2.10 — the latest release is v0.2.10.
  2. window.Ask2Do.version in the console.
  3. 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:

html
<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:

html
<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)

html
<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)

html
<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)

javascript
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):

  1. window.Ask2DoUser = { id, role } set before the widget script (best for SPAs).
  2. data-panel-user + data-panel-role attributes (best for server-rendered HTML).
  3. 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

AttributeRequiredDescription
data-cloud-urlYesWebSocket origin. Almost always wss://cloud.ask2do.com.
data-token-endpointOne of token-endpoint or Ask2DoGetTokenURL the widget fetches with credentials: "include" to mint a JWT. Ignored if window.Ask2DoGetToken is defined.
data-panel-userOptionalString id of the signed-in admin. Appears in audit logs.
data-panel-roleOptionaladmin or owner. Other values hide the button.
deferRecommendedLets the widget mount after DOM ready without blocking rendering.

Window globals

GlobalDirectionDescription
window.Ask2DoUserYou set{ id?: string; role: string }. Read once at widget construction.
window.Ask2DoGetTokenYou set() => Promise<string> — called fresh on every modal open.
window.Ask2Do.versionWidget setsString. Useful for support tickets and analytics.
window.Ask2Do.mount(opts)Widget setsImperative 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

css
.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:

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:

html
<!-- 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):

javascript
// 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;

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.role one of admin or owner?
  • Has your panel set display: none on fixed elements globally? Button uses position: 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/token with your tenant key in Authorization: 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/owner roles based on client-side panelUser.
  • 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