AIRA — Widgets Agent demo

Open science, in the open

A mock Frontiers landing page that every block below exercises one capability of AIRA, the embedded assistant. Open the assistant in the bottom-right , follow the suggested prompt, and watch the matching block light up.

Bound to widget pk_test_demo. Make sure its pinned agent has the Widget Host Events tool enabled in Settings > Agents.

1. Browse — scroll-to-section navigation

Fire-and-forget emit_host_event. AIRA picks the right section based on the conversation; your handler calls scrollIntoView on the matching element.

Try: "Take me to the journals" or "Show me the FAQ"

Journals

Open-access journals across the natural and social sciences. When AIRA calls show_journals, this card scrolls into view and flashes.

FAQ

Common author questions — review timelines, copyright, withdrawal policy. Ask AIRA "what's the average review time?" to scroll here.

JavaScript
// 1. Fire-and-forget navigation — scroll to a section.
AssistantGPTWidget('on', 'event', (name) => {
  if (name === 'show_journals') {
    document.querySelector('#section-journals')
      ?.scrollIntoView({ behavior: 'smooth', block: 'center' })
  }
  if (name === 'show_faq') {
    document.querySelector('#section-faq')
      ?.scrollIntoView({ behavior: 'smooth', block: 'center' })
  }
})

2. Article Processing Charges (APCs)

AIRA can pass a data payload alongside the event name. Here it uses show_apcs({ tier }) to highlight a specific APC tier as the conversation narrows in.

Try: "How much does it cost to publish in a Frontiers journal?" or "What's the APC for a high-impact journal?"

Bronze

$1,490

per accepted article

  • • Spotlight journals
  • • Up to 50 references
  • • 60-day median review

Standard

$2,490

per accepted article

  • • Most disciplinary journals
  • • Unlimited references
  • • Full peer review

Premium

$2,990

per accepted article

  • • High-impact / flagship journals
  • • Expedited review
  • • Featured-article eligibility

APCs vary by journal field of study. Waivers and discounts available for authors from low- and middle-income economies.

JavaScript
// 2. Fire-and-forget WITH a data payload — highlight a tier.
AssistantGPTWidget('on', 'event', (name, data) => {
  if (name !== 'show_apcs') return
  const tier = data?.tier ?? 'standard' // 'bronze' | 'standard' | 'premium'
  highlightTier(tier)
})

3. Manuscript submission

Authors describe their paper in chat; AIRA emits prefill_submission with whatever it gathered so the form is pre-populated by the time they scroll down. Pure side effect — no round-trip needed.

Try: "I'd like to submit a neuroscience paper. I'm Dr. Maria Sato, maria@univ.test, and the working title is 'Cortical plasticity in adult learning'."

Last submission

— nothing yet —
JavaScript
// 3. Prefill a form from the agent's gathered fields.
AssistantGPTWidget('on', 'event', (name, data) => {
  if (name !== 'prefill_submission') return
  if (typeof data?.name === 'string') form.name.value = data.name
  if (typeof data?.email === 'string') form.email.value = data.email
  if (typeof data?.journal === 'string') form.journal.value = data.journal
  if (typeof data?.title === 'string') form.title.value = data.title
  if (typeof data?.abstract === 'string') form.abstract.value = data.abstract
})

4. Reading list — round-trip RPC

AIRA calls request_host_event when it needs a value back from the page. Here it asks "what's in the reader's saved-articles list?" before answering. Your handler calls reply({ items, count }) and AIRA uses the value verbatim.

Try: "What's in my reading list?" (round-trip) or "Add Frontiers in Neuroscience to my list" (fire-and-forget add_to_reading_list).

Browse journals

  • Frontiers in NeuroscienceFNINS

    Neuroscience · APC tier Premium

  • Frontiers in Plant ScienceFPLS

    Plant Science · APC tier Standard

  • Frontiers in Artificial IntelligenceFRAI

    AI / Machine Learning · APC tier Standard

  • Frontiers in MedicineFMED

    Medical Sciences · APC tier Premium

  • Frontiers in Environmental ScienceFENVS

    Environmental Science · APC tier Bronze

Your reading list

Reading list is empty — ask AIRA to add a journal.

Count0

Available events: add_to_reading_list({ code }), clear_reading_list, read_reading_list (request).

JavaScript
// 4. Two-way request — reply with a plain, cloneable object.
AssistantGPTWidget('on', 'event', (name, data, reply) => {
  switch (name) {
    case 'add_to_reading_list': // emit (fire-and-forget)
      if (data?.code) addJournal(data.code)
      break
    case 'clear_reading_list':  // emit
      readingList = []
      break
    case 'read_reading_list':   // request — reply within 10s
      reply({ items: readingList, count: readingList.length })
      break
  }
})

5. Identity context — round-trip role lookup

Flip the role below to simulate different Frontiers users. Ask AIRA "what's my role?" and it'll call request_host_event({ name: 'get_user_role' }); your handler replies with the current value. Async-friendly — handlers can await work before replying inside the 10s default timeout.

Try: "Am I signed in?" or "What's my role on Frontiers?" (cycle the role below, then ask again to see the answer change).
Current visitor:Anonymous
JavaScript
// 5. Async round-trip — await work, then reply within 10s.
AssistantGPTWidget('on', 'event', async (name, data, reply) => {
  if (name !== 'get_user_role') return
  const role = await fetchCurrentRole()
  reply(role) // primitives are fine; objects must be plain/cloneable
})

7. Direct JSAPI controls

These don't come from AIRA — they're the public window.AssistantGPTWidget(…) calls Frontiers' own code makes for show / hide / shutdown / analytics / proactive nudges.

The page also fires an auto-nudge ~3 seconds after first paint. Once you click × on the bubble (or click into it) the loader stores a per-widget sessionStorage marker so you don't get re-nudged on SPA navs. The button below passes force: true so you can re-trigger it from this section for testing without clearing storage.

JavaScript
// 7. Host-driven controls (your own code, not the agent).
AssistantGPTWidget('show')
AssistantGPTWidget('hide')

AssistantGPTWidget('nudge', {
  message: 'Looking at APCs? I can match your manuscript to the right tier.',
  delay_ms: 0,
  prefill: 'My paper is in neuroscience and ~8000 words. Which APC tier?',
  force: true,
})
AssistantGPTWidget('nudge', null) // dismiss

AssistantGPTWidget('trackEvent', 'demo_cta_click', { page: 'widget-demo' })
AssistantGPTWidget('shutdown')

API surface

Every public command on window.AssistantGPTWidget, with the full set of supported fields. The live demos above exercise each one against the running widget.

Calling shape is Intercom-style: AssistantGPTWidget(command, ...args). The install snippet stubs this as a queueing function until loader.js arrives and drains the queue — calls fired before boot still land in order.

Install snippet
<!-- Install snippet — drop once before </body>. -->
<script>
  window.AssistantGPTWidgetSettings = {
    organisation_uid: 'org-frontiers',
    widget_key: 'pk_live_...',     // public widget key
    capture_page_context: true,    // optional
  }
  window.AssistantGPTWidget = window.AssistantGPTWidget || function () {
    (window.AssistantGPTWidget.q = window.AssistantGPTWidget.q || []).push(arguments)
  }
  var s = document.createElement('script')
  s.src = 'https://<your-deployment>/embed/widget.js'
  s.async = 1
  document.head.appendChild(s)
</script>

Then drive it with any of the commands below.

AssistantGPTWidget('boot', settings)#

Initialise the widget. Normally called once by the install snippet, but you can call it again at runtime to re-bind to a different widget key or user. Resolves nothing — fires the 'boot' event on success.

organisation_uidrequired
string — the Frontiers organisation that owns this widget (e.g. org-frontiers).
widget_keyrequired
string — public widget key (pk_live_...) that scopes the agent, corpus, tools, and quotas.
user_id
string? — Phase 2 identity: durable visitor id known to the host app. Paired with user_hash; ignored on its own.
user_hash
string? — Phase 2 identity: HMAC-SHA256 of user_id keyed by the widget's signing_secret. Mint server-side; same shape as Intercom Identity Verification.
user
object? — descriptive profile fields shown in the assistant UI (name, email, …). Never used for authorisation — only the (user_id, user_hash) pair is trusted.
custom_attributes
object? — arbitrary structured properties attached to the visitor for the agent's context (plan tier, signup date, cart total…). Plain JSON only.
capture_page_context
boolean? — when true, forwards a snapshot of the page (URL, title, visible text capped at 6 KB) to the iframe whenever the panel opens. Off by default — opt in only where the host page's content isn't sensitive.
api_base
string? — override the server origin the iframe and JSAPI talk to. Defaults to the origin loader.js was served from. Useful for self-hosted mirrors.
alignment
'right' | 'left'? — which corner to dock the launcher in. Defaults to right.
horizontal_padding
number? — pixel offset from the docked corner along the X axis. Default 20.
vertical_padding
number? — pixel offset from the docked corner along the Y axis. Default 20.
open
boolean? — auto-open the panel right after boot. Intercom-compat. Off by default.

Giving the agent context — three patterns, pick by recency. The widget deliberately separates static, snapshot, and dynamic context so the agent sees fresh data without the host having to re-boot the widget on every page change.

custom_attributesstatic, mutable. Set on boot, patch via update. Use for plan tier, signup date, role tags — things that change rarely.

capture_page_contextsnapshot, opt-in. When true, the loader forwards the page's URL + title + visible text (6 KB cap) once per panel open. Use for "summarise this article" / "what does this say about X" flows on content-heavy pages.

request_host_eventdynamic, per-turn. Documented under on / off below. The agent calls a named host event from a tool call; your on('event', (name, data, reply) => reply(...)) handler returns a structured value the LLM consumes in the next turn. Use for cart contents, current role, form state — anything that changes during the conversation and the agent needs right now.

JavaScript
// Normally fired by the install snippet, but you can re-boot at
// runtime to re-bind to a different widget key or user.
AssistantGPTWidget('boot', {
  organisation_uid: 'org-frontiers',
  widget_key: 'pk_live_...',
  user_id: 'usr_123',          // optional, paired with user_hash
  user_hash: '<hmac-sha256>',  // mint server-side
  custom_attributes: { plan: 'premium' },
  capture_page_context: true,
})
AssistantGPTWidget('update', settings)#

Patch the current settings without re-booting. Typical use: SPA navigation flips context.page on every route change so the agent always knows where the visitor is.

Accepts a partial of the boot payload. Fields you omit are left untouched.

JavaScript
// Patch settings without re-booting (e.g. on SPA route change).
AssistantGPTWidget('update', {
  custom_attributes: { plan: 'premium', last_route: location.pathname },
})
AssistantGPTWidget('show') / ('hide')#

Open or close the chat panel programmatically. Use show on a custom "Ask the assistant" CTA to skip the launcher FAB; hide if you're driving the widget from a wizard step. Fires the 'show' / 'hide' events.

No parameters.

JavaScript
AssistantGPTWidget('show')
AssistantGPTWidget('hide')
AssistantGPTWidget('nudge', opts | null)#

Show a proactive speech bubble next to the launcher. Click the bubble → the chat opens with the optional prefill pre-typed in the composer. Pass null to dismiss any current nudge programmatically.

messagerequired
string — bubble text.
delay_ms
number? — delay in milliseconds before the bubble appears. Default 0.
prefill
string? — text to drop into the composer when the bubble is clicked. The panel opens either way; prefill makes "click bubble → ask the obvious question" a one-tap interaction.
force
boolean? — re-show even if the visitor dismissed a previous nudge this session. Defaults to false (dismissals persist via sessionStorage). Usually only set for testing.
JavaScript
AssistantGPTWidget('nudge', {
  message: 'Need help choosing a journal?',
  delay_ms: 3000,
  prefill: 'Which Frontiers journal fits a neuroscience paper?',
  force: false,
})

AssistantGPTWidget('nudge', null) // dismiss the current nudge
AssistantGPTWidget('trackEvent', name, metadata?)#

Forward a host-side analytics event into the widget's telemetry pipeline (and back out as a 'track' event for adopters who pipe to their own analytics). Useful for correlating widget conversations with funnel steps the assistant doesn't observe directly.

namerequired
string — event name (e.g. submission_started).
metadata
object? — arbitrary structured properties to attach.
JavaScript
AssistantGPTWidget('trackEvent', 'submission_started', { journal: 'FNINS' })
AssistantGPTWidget('shutdown')#

Tear down the widget: remove the launcher, close the panel, drop all event listeners. The visitor loses access until you call boot again. Use at logout — especially when a different user is about to sign in on the same page so their thread history doesn't carry over.

No parameters. Fires 'shutdown'.

JavaScript
// Tear down at logout (drop launcher, panel, listeners).
AssistantGPTWidget('shutdown')
AssistantGPTWidget('on', event, handler) / ('off', event, handler)#

Subscribe to events the widget fires back to the host page. Pure synchronous registration — no promise. off removes a previously-registered handler.

'event'
LLM-driven host event. Handler receives (name, data, reply?). When reply is present, the agent called request_host_event and is awaiting a structured response — call reply(payload) (within 10s) to feed the data back into the next turn. Fire-and-forget when reply is omitted.
'boot'
Fires once when the loader has finished setting up. Handler receives { loader_id, version }.
'show' / 'hide'
Fired when the chat panel opens or closes — whether triggered by the launcher, the JSAPI, or a nudge click. Use to mirror open/close state in your own UI.
'track'
Echo of a trackEvent call from the same page. Handler receives (name, metadata). Lets adopters fan analytics calls out to their own observability stack without intercepting trackEvent directly.
'shutdown'
Fired after shutdown finishes tearing down. Use to clear any UI mirroring the widget's state on the host page.
JavaScript
// LLM-driven host events use the (name, data, reply) shape.
const handler = (name, data, reply) => {
  if (name === 'get_user_role') reply?.(currentUser?.role ?? null)
}
AssistantGPTWidget('on', 'event', handler)

// Lifecycle events deliver their own args.
AssistantGPTWidget('on', 'show', () => console.log('panel opened'))
AssistantGPTWidget('on', 'track', (name, metadata) => forward(name, metadata))
AssistantGPTWidget('on', 'shutdown', () => clearWidgetUi())

AssistantGPTWidget('off', 'event', handler) // unsubscribe

Source of truth: apps/assistant-embed/src/api.ts (commands + signatures) and apps/assistant-embed/src/loader.ts (which events fire when).

© 2026 Frontiers Media SADemo harness — widget bundle served from this assistant-ui deployment at /embed/widget.js.
Event sink (latest 10)0
(no events yet — ask AIRA to do something)