Template (HTML widget)
Overview
The purpose of using Template (HTML widget) as a response handling method is to create dynamic, interactive HTML widgets that enrich the chat experience and present information or functionality directly within the conversation. This approach provides a flexible canvas for integrating a vast range of client-side UI elements, whether they depend on external data sources or not. For instance, you could create a mortgage calculator for a banking chatbot that requires no external data, a Google Chart that fetches analytics from a REST or GraphQL endpoint, or even an embedded iframe to display external web content on-demand. Beyond these examples, the possibilities are virtually limitless:
Data flow for when Handle Response is Template (HTML Widget)
In this example we do have a Rest API
, the Data Source is not required to create a Template (HTML widget)
The Rest API
Returns:
[
[
"Task",
"Hours per Day"
],
[
"Work",
11
],
[
"Eat",
2
],
[
"Commute",
2
],
[
"Watch TV",
2
],
[
"Sleep",
7
]
]
- An interactive currency converter fetching live exchange rates from a REST API.
- A product recommendation carousel, integrated with a CRM or e-commerce system, to show personalized items.
- A data-driven timeline of events or milestones, pulling structured historical data via GraphQL.
- A polling or voting widget that updates results in real-time, drawing data from a dynamic backend.
- A simple guestbook or Q&A form that requires no external data, allowing users to submit feedback or questions within the chat window.
By leveraging Template (HTML widget), developers and businesses can effortlessly blend data retrieval, processing, and presentation into a cohesive, visually appealing, and highly interactive chat-based user experience.
In the Edit Parameter dialog it will define what will show up in this Edit Integration dialog.
For this example, we set the Handle Response -> Template (HTML widget)
, and Data Source -> Rest API
See the Edit Parameter dialog below:
Google Charts, displayed in Chat
When using Template (HTML widget):
-
The template receives two main objects:
llm
: Contains LLM parameters, includingllm.llm_query
, it is always passed to the Integration.source
: Contains the processed data from your chosen data source (if any), potentially post processed by JSONata, and potentially processed by Python.
-
Insert handlebar tags like
{{llm.llm_query}}
or{{source.<fieldName>}}
into the HTML to dynamically populate the UI. -
msgId
is unique id for this bubble. This Way you can call the widget more than one time and not have a conflict with ids -
For example, if
source
contains an array of data representing chart values, you can use JavaScript (within your HTML template) to load a Google Chart library and passsource
as the data array to render a visual chart. -
ai12z uses
shadowRoot
so not to conflict with the clients javascript or CSS. Note how to access the ai12z-cta and html tags in the bubble -
Note:
processArray source
when source is an array include processArray in Handlebars -
msgId
this is a unique ID for the bubble that the html template HTML widget is in -
ai12zChat[
{{msgId}}
].onLoadCallback Executes after the bubble template HTML widget is finished loading into the bot. Note this is important, you are waiting for the bubble to render so javascript is valid. - Referemce Handlebars
HTML code:
<div id="{{msgId}}" style="width: 500px; height: 500px"></div>
<script
async
type="text/javascript"
src="https://www.gstatic.com/charts/loader.js"
></script>
<script type="text/javascript">
// Define the data for the pie chart from the REST API endpoint
// If `source` is an array using Handlebars, it needs to be processed
var chartData = {{processArray source}};
// Function to draw the pie chart
function loaddata() {
// Convert the data into a DataTable format
const data = google.visualization.arrayToDataTable(chartData);
// Define the chart options
const options = {
title: "{{llm.bot_query}}",
};
// Check for `ai12z-cta` elements in the DOM and process them
if (document.querySelector("ai12z-cta")) {
const listElements = document.querySelectorAll("ai12z-cta");
listElements.forEach((ctaElement, index) => {
// Access the shadow DOM of each `ai12z-cta` element
const chatElement = ctaElement.shadowRoot?.querySelector("ai12z-chat");
if (chatElement) {
const chartContainer = chatElement.shadowRoot?.querySelector(`#{{msgId}}`);
if (chartContainer) {
// Initialize and draw the chart
const chart = new google.visualization.PieChart(chartContainer);
chart.draw(data, options);
} else {
console.log(`Chart container not found for element at index ${index}`);
}
} else {
console.log(`ai12z-chat not found in shadow DOM of element at index ${index}`);
}
});
} else {
console.log("No ai12z-cta elements found in the DOM");
}
}
// Wait for the bubble to load with code
ai12zChat[`{{msgId}}`].onLoadCallback = () => {
// Load the Google Charts package
google.charts.load("current", { packages: ["corechart"] });
// Set a callback to draw the chart once the package is loaded
google.charts.setOnLoadCallback(loaddata);
};
</script>
Integration dialog
Example 2: Ski Selector HTML Widget
This example demonstrates an interactive ski matching quiz widget that recommends the best skis based on user-selected preferences. The widget collects answers from a multiple-choice form and, upon clicking Show My Match, displays a recommended ski model with a "Show More" button for additional details.
Widget Screenshots (Newest First)
1. Quiz Start (Step 1)
2. Show More Button (Card View)
3. Widget Properties (Integration Settings)
4. Widget Edit (HTML Editor)
Features
- Dynamic Quiz: Users answer questions about skill level, terrain, style, and location.
- Smart Recommendation: The widget matches the best ski based on input using a simple rule engine.
- Show More Button: The result card includes a "Show More" button, which can trigger additional info (e.g., detailed product info, cart integration).
- Shadow DOM Support: All form and result elements are rendered inside the widget’s Shadow Root. JavaScript should be placed in the Web Controls → <ai12z-bot> → Javascript tab to work correctly.
- Easy Customization: HTML and JavaScript can be edited directly in the integration’s Template (HTML) editor.
How it Works
HTML Template
Paste this into the integration’s HTML template:
<div style="font-family: Arial, sans-serif; margin: 10px;">
<h3>Find Your Perfect Skis</h3>
<form id="ski-form" autocomplete="off">
<div>
<p><strong>1. What is your skill level?</strong></p>
<label
><input type="radio" name="skill" value="Beginner" required />
Beginner</label
><br />
<label
><input type="radio" name="skill" value="Intermediate" />
Intermediate</label
><br />
<label><input type="radio" name="skill" value="Expert" /> Expert</label>
</div>
<div>
<p><strong>2. What terrain do you ski in the most?</strong></p>
<label
><input type="radio" name="terrain" value="Groomed" required />
Groomed</label
><br />
<label
><input type="radio" name="terrain" value="Powder" />
Powder/Off-piste</label
><br />
<label
><input type="radio" name="terrain" value="Park" /> Terrain Park</label
><br />
<label
><input type="radio" name="terrain" value="Mixed" /> A bit of
everything</label
>
</div>
<div>
<p><strong>3. What is your skiing style?</strong></p>
<label
><input type="radio" name="style" value="Technical" required />
Controlled and technical</label
><br />
<label
><input type="radio" name="style" value="Aggressive" /> Fast and
aggressive</label
><br />
<label
><input type="radio" name="style" value="Playful" /> Playful and
relaxed</label
><br />
<label
><input type="radio" name="style" value="Figuring" /> Still figuring it
out</label
>
</div>
<div>
<p><strong>4. Where do you ski most?</strong></p>
<label
><input type="radio" name="location" value="Northeast" required />
Northeast U.S. (often icy)</label
><br />
<label
><input type="radio" name="location" value="West" /> Rockies /
West</label
><br />
<label><input type="radio" name="location" value="Europe" /> Europe</label
><br />
<label
><input type="radio" name="location" value="Wherever" /> Wherever
there's snow</label
>
</div>
<button
type="button"
onclick="handleSkiFormSubmitFromButton(this)"
style="margin-top: 10px; background-color: #63dadf; border: none; padding: 10px 16px; color: white; border-radius: 4px; cursor: pointer;"
>
Show My Match
</button>
</form>
<div id="ski-result" style="margin-top: 20px;"></div>
</div>
JavaScript Logic
Place this in the Web Controls → <ai12z-bot> → Javascript tab. Note: The widget runs inside a Shadow Root. Access elements via the event’s scope or use relative selectors.
function handleSkiFormSubmitFromButton(button) {
// Find the nearest form
const form = button.closest("form")
if (!form) return
const skill = form.querySelector('input[name="skill"]:checked')?.value
const terrain = form.querySelector('input[name="terrain"]:checked')?.value
const style = form.querySelector('input[name="style"]:checked')?.value
const match = findSkiMatch(skill, terrain, style)
const resultDiv = form.parentNode.querySelector("#ski-result")
if (resultDiv) {
resultDiv.innerHTML = `
<div style="display: flex; flex-wrap: wrap; border: 1px solid #ccc; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1); max-width: 600px;">
<img src="${match.image}" alt="${match.name}" style="width: 200px; object-fit: cover;" />
<div style="padding: 16px; flex: 1;">
<h4 style="margin-top: 0;">${match.name}</h4>
<p style="margin: 8px 0;">${match.type}</p>
<button type="button" class="card-button list-btn uppercase flex-grow hover:bg-gradient-to-t md:flex-none" onclick='ai12zBot.sendMessage("${match.name}","${match.name}")'>Show More</button>
</div>
</div>
`
}
}
// Simple logic for demo. Expand this function to add more rules!
function findSkiMatch(skill, terrain, style) {
// Default match
let match = {
name: "Nordica Enforcer 94",
type: "All-Mountain skis",
image:
"https://cdn.shopify.com/s/files/1/0920/9112/1967/files/700_253125_1081575.webp?v=1751427036",
link: "https://ecom.ai12z.com/nordica-enforcer-94-all-mountain-skis-2025/",
}
if (skill === "Expert" && terrain === "Powder") {
match = {
name: "Faction La Machine 5",
type: "Powder skis",
image:
"https://cdn.shopify.com/s/files/1/0920/9112/1967/files/Faction-Skis-2526-La-Machine-5-Base-1x1.webp?v=1751438691",
link: "https://ecom.ai12z.com/faction-skis-la-machine-5-powder/",
}
} else if (terrain === "Park" || style === "Playful") {
match = {
name: "Völkl Revolt 84",
type: "Freestyle skis",
image:
"https://cdn.shopify.com/s/files/1/0920/9112/1967/files/volkl_2425_revolt-84_V2410156_1.webp?v=1751439056",
link: "https://ecom.ai12z.com/volkl-revolt-84-skis-2025/",
}
} else if (terrain === "Groomed" && style === "Technical") {
match = {
name: "Salomon S/Max 8",
type: "Carving skis",
image:
"https://cdn.shopify.com/s/files/1/0920/9112/1967/files/L47652900_0_SKE_ES_MAX8_M11GW.png.cq5dam.web.1200.1200_2048x2048_cf50fb8e-9cf2-4e63-bd57-9ef359dbea60.webp?v=1751428016",
link: "https://ecom.ai12z.com/2026-s-max-8-carving-ski-w-binding/",
}
}
return match
}
The "Show More" Button
- Purpose: Triggers a message to the copilot with the matched ski’s name. This can be hooked to display product detail, start a cart, or provide more information in the chat.
- Placement: Appears on the recommendation card beneath the matched ski info.
- Behavior: Calls
ai12zBot.sendMessage
with the ski name, leveraging the AI12Z copilot for contextual response.
Notes
-
Shadow Root/Encapsulation: All widgets are rendered in a Shadow DOM for style and behavior isolation. To interact with elements, use local selectors relative to the event, not
document.querySelector
. -
Where to Put JS: Paste your JavaScript in the Web Controls → <ai12z-bot> → Javascript tab for it to work within the widget's shadow root.