Custom Carousel
You can create a custom carousels either a list or slider, with Handlebars
What is Handlebars
Handlebars is a popular templating language used in web development. It helps you generate HTML dynamically by combining templates (HTML with special tags) and JSON data, that comes from an endpoint that you have specified. Introduction Handlebars
Endpoint data requirements
Your endpoint, ideally return an object that contains items array at the root. Within an item you can have other arrays. You can not have other arrays at the root.
When your endpoint returns array that is not named items
When interacting with an endpoint, like shopify MCP, when using the search_shop_catalog. It returns an array of products, you have to transform this JSON to be an array of items
Using JSONata Response for the transformation
{
"items": products,
"pagination": pagination,
"available_filters": available_filters,
"instructions": instructions
}
Which will generate the JSON that now has an array of items
-- Absolutely, here’s a cleaned-up, concise, and structured version that improves clarity and flow. I’m also correcting typos and tightening up the decision tree.
Endpoint Data Handling
When should endpoint data go to the LLM, the Carousel (bypassing the LLM), or both?
1. No Carousel Items:
- If the Carousel exists but
items.length === 0
, the LLM processes the returned data (which contains no items) and typically responds with "no results found" along with possible recommendations.
2. Carousel Items Exist — Data Routing Rules: Data routing is controlled by two parameters:
- The endpoint JSON object property:
process
- The LLM response parameter:
llmCarouselBothProcess
Routing Logic:
-
If the
process
parameter is set:-
process = "llm"
:- Data is not sent to the Carousel.
- The LLM processes the JSON and generates the response.
-
process = "both"
:- Data is sent to both the LLM and the Carousel.
- The LLM processes the JSON and provides additional commentary or context around the Carousel.
-
-
If the LLM response includes
llmCarouselBothProcess
:-
llmCarouselBothProcess = "llm"
:- Data is not sent to the Carousel.
- The LLM processes the JSON exclusively.
-
llmCarouselBothProcess = "both"
:- Data is sent to both the LLM and the Carousel.
- The LLM processes the data and enriches the Carousel with additional context.
-
-
If neither
process
norllmCarouselBothProcess
is set to"llm"
or"both"
:- Data bypasses the LLM and is sent directly to the Carousel for rendering.
Summary Table
Condition | Sent to LLM | Sent to Carousel | Notes |
---|---|---|---|
Carousel exists, items.length === 0 | ✔️ | ❌ | LLM handles empty results |
process = "llm" | ✔️ | ❌ | LLM only |
process = "both" | ✔️ | ✔️ | Both LLM and Carousel |
llmCarouselBothProcess = "llm" | ✔️ | ❌ | LLM only |
llmCarouselBothProcess = "both" | ✔️ | ✔️ | Both LLM and Carousel |
(Default: neither flag set to "llm"/"both") | ❌ | ✔️ | Carousel only, bypassing LLM |
Real Estate Example for appartment rentals
In this example, there is a list of buildings, which is the items array. Each item has an array of units.
The Carousel Controls accepts structured data to display a series of interactive items, each potentially with multiple panels, forms, images, and detailed content. Each item can include titles, images, descriptions, forms, and buttons with various functions. Below is a comprehensive guide on how to pass data to the Carousel Control and define the behavior of the interactive elements.
Tabs
The first tab represents the items, it is the Handlebars template for creating the cards. One tab is required, other tabs are optional. Additional tabs are panel views of that item information. The name of the tab is what you use to add buttons to navigage between the different panel.
Tab "List"
{{#each items as |item index|}}
<div class="carousel-card flex flex-wrap items-start mb-4 bg-white border h-auto border-gray-200 rounded-lg shadow md:flex-row dark:border-gray-700" data-index="{{index}}">
<div class="relative md:w-1/3 carousel-container card-image {{#if (gte item.images.length 1)}}w-full{{else}}w-full md:w-1/2{{/if}}" data-carousel-id="carousel-{{index}}" id="carousel-{{index}}" data-carousel="slide">
<div class="relative h-64 overflow-hidden">
{{#each item.images as |image imageIndex|}}
<img src="{{image}}" alt="Image for {{../name}}" class="card-image object-cover w-full h-96 md:h-auto md:mt-6 md:ml-2 hidden duration-700 ease-in-out" data-carousel-item id="carousel-{{index}}-item-{{imageIndex}}"/>
{{/each}}
</div>
{{#if (lte item.images.length 3)}}
{{#if (gte item.images.length 1)}}
<div class="absolute z-30 flex -translate-x-1/2 bottom-5 left-1/2 space-x-3 rtl:space-x-reverse">
{{#each item.images as |image imageIndex|}}
<button type="button" class="w-3 h-3 rounded-full" aria-current="true" aria-label="Slide {{imageIndex}}" data-carousel-slide-to="{{imageIndex}}" id="carousel-{{index}}-indicator-{{imageIndex}}"></button>
{{/each}}
</div>
{{/if}}
{{else}}
<div class="absolute z-30 flex -translate-x-1/2 bottom-5 left-1/2 space-x-3 rtl:space-x-reverse">
<button type="button" class="w-3 h-3 rounded-full bg-blue-600" aria-current="true" aria-label="Previous Slide" data-carousel-prev id="carousel-{{index}}-prev"></button>
<button type="button" class="w-3 h-3 rounded-full bg-blue-600" aria-current="true" aria-label="Next Slide" data-carousel-next id="carousel-{{index}}-next"></button>
</div>
{{/if}}
</div>
<div class="flex flex-col justify-between p-4 leading-normal md:w-2/3 card-right">
<h5 class="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white">{{item.name}}</h5>
<p short-description mb-3 font-normal text-gray-700 dark:text-gray-400">
Distance: {{item.distance}} Km
</p>
<p class="font-title text-md md:text-xl">
{{item.location.Address}}, {{item.location.City}}, {{item.location.ProvinceCode}} {{item.location.PostalCode}}
</p>
{{#if item.suites.length}}
<p class="font-title text-md md:text-xl">
{{item.suites.[0].bedrooms}} Bed / {{item.suites.[0].bathrooms}} Bath – {{item.suites.[0].squareFeet}} sq ft – ${{item.suites.[0].rate}} CAD/month
</p>
{{/if}}
<p class="short-description mb-3 font-normal text-gray-700 dark:text-gray-400">
{{{item.buildingDescription}}}
</p>
<div class="flex items-center my-4">
<button class="card-button list-btn uppercase flex-grow hover:bg-gradient-to-t md:flex-none" data-type="panel" onclick='carousel.selectByName("Units")'>Units</button>
<button class="card-button list-btn uppercase flex-grow hover:bg-gradient-to-t md:flex-none" data-type="panel" onclick='carousel.selectByName("Info")'>Info</button>
{{#if ../isBot }}
<button class="card-button list-btn uppercase flex-grow hover:bg-gradient-to-t md:flex-none" onclick='ai12zBot.sendMessage("Request a Tour form","Request a Tour")'>Book Tour</button>
{{else}}
<button class="card-button list-btn uppercase flex-grow hover:bg-gradient-to-t md:flex-none" onclick='ai12zCta.sendMessage("Request a Tour form","Request a Tour")'>Book Tour</button>
{{/if}}
</div>
</div>
</div>
{{/each}}
<div class="pagination-controls">
<button class="prev-btn list-btn" {{#unless hasPrev}}disabled{{/unless}}>Prev</button>
<span> Page {{getcurrentPage}} of {{getPageCount}} </span>
<button class="next-btn list-btn" {{#unless hasNext}}disabled{{/unless}}>Next</button>
</div>
Tab "Units"
{{#each item.suites as |suite index|}}
<div class="card">
<div class="card-content">
<h3 class="card-title">
<b
>Suite {{inc index}}: {{suite.bedrooms}} Bed / {{suite.bathrooms}}
Bath</b
>
</h3>
<p class="card-subtitle">
{{suite.squareFeet}} sq ft - ${{suite.rate}} CAD/Month
</p>
<p class="card-subtitle">Available: {{suite.availabilityDate}}</p>
</div>
<img
class="card-img"
src="{{suite.floorplanImage}}"
alt="Floor Image of {{title}}"
/>
</div>
{{/each}}
<button
class="panel-button list-btn fav-btn"
data-type="panel"
onclick='carousel.selectByName("List")'
data-id="0"
>
Add to favourites
</button>
<button
class="panel-button list-btn text-white"
data-type="panel"
onclick='carousel.selectByName("List")'
>
Back
</button>
<style>
.card {
border: 1px solid #ddd;
/*border-radius: 8px;*/
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
font-family: sans-serif;
margin: 16px;
}
.card-content {
padding: 16px;
}
.card-title {
margin: 0;
font-size: 1.2em;
font-weight: bold;
}
.card-subtitle {
margin-top: 8px;
font-size: 0.95em;
color: black;
}
.card-img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.fav-btn {
margin: 10px;
}
</style>
Tab "Info"
<div class="amenities-section p-4 bg-gray-50 rounded-md shadow-md w-full">
<h4 class="text-lg font-semibold text-gray-800 mb-2">Amenities</h4>
<ul class="list-disc list-inside text-gray-700 space-y-1">
{{#each item.amenities}}
<li>{{this}}</li>
{{/each}}
</ul>
<h4 class="text-lg font-semibold text-gray-800 mb-2">Utilities</h4>
<ul class="list-disc list-inside text-gray-700 space-y-1">
{{#each item.utilities}}
<li>{{this}}</li>
{{/each}}
</ul>
<p>
<b>Contact Info: </b> {{item.location.Address}}, {{item.location.City}},
{{item.location.Country}}
</p>
<button
class="panel-button list-btn text-white"
data-type="panel"
onclick='carousel.selectByName("List")'
>
Back
</button>
</div>
Buttons
Hyperlink button
<button class="panel-button list-btn text-white" onclick="window.open('https://www.ai12z.com', '_blank')">ai12z</button>
Panel button, to list note class is panel-button
<button class="panel-button list-btn text-white" data-type="panel" onclick='carousel.selectByName("List")'>Back</button>
Panel button, to any other tab, class is card-button
<button class="card-button list-btn uppercase flex-grow hover:bg-gradient-to-t md:flex-none" data-type="panel" onclick='carousel.selectByName("Units")'>Units</button>
Using sendMessage or sendJSON, requires knowing which type of client side control is being used, Bot or Search
{{#if ../isBot }}
<button class="card-button list-btn uppercase flex-grow hover:bg-gradient-to-t md:flex-none" onclick='ai12zBot.sendMessage("Request a Tour form","Request a Tour")'>Book Tour</button>
{{else}}
<button class="card-button list-btn uppercase flex-grow hover:bg-gradient-to-t md:flex-none" onclick='ai12zCta.sendMessage("Request a Tour form","Request a Tour")'>Book Tour</button>
{{/if}}
JavaScript functions
Shadow Root
Shadow Root allows ai12z bot widget to encapsulate its HTML, CSS, and JavaScript, preventing the host website’s styles or scripts from interfering with your bot’s appearance or behavior. This isolation ensures your chatbot always looks and functions as intended, no matter where it’s embedded. For bot providers, this means fewer conflicts, easier maintenance, and a more reliable user experience across all customer sites.
Where to add your Script
Add your javascript to the bot, config under the tab Script
, do not add to the handlebars template. Note: You cannot add it outside of the script tag, because the code is modified to work with a shadow root. To if you put it in an external js file it will not work.
function openLink() {
window.open("https://www.google.com", "_blank")
}
<button class="panel-button list-btn text-white" onclick="openLink()">
ai12z
</button>
Example using JavaScript within Shadow Root
Handelbars for Menu Example
In this example, we show how to select the QNTY, when clicking Add to Cart
{{#each items}}
<div
class="carousel-card flex flex-wrap items-start mb-4 bg-white border h-auto border-gray-200 rounded-lg shadow"
>
<!-- Image Panel -->
<div class="relative w-full md:w-1/3 carousel-container card-image">
{{#if images.[0]}}
<img
src="{{images.[0]}}"
alt="Image for {{title}}"
class="card-image object-cover w-full h-64 md:h-80 duration-700 ease-in-out"
/>
{{/if}}
</div>
<!-- Details Panel -->
<div
class="flex flex-col justify-between p-4 leading-normal md:w-2/3 card-right"
>
<h5 class="mb-2 text-2xl font-bold tracking-tight text-gray-900">
{{title}}
</h5>
<ul class="subtitles mb-1 text-sm text-gray-600">
{{#each subTitle}}
<li>{{this}}</li>
{{/each}}
</ul>
<p class="short-description mb-3 font-normal text-gray-700">
{{shortDescription}}
</p>
<!-- Quantity and Button -->
<div class="flex items-center my-4">
<label for="qnty-{{@index}}" class="mr-2">Qnty:</label>
<select id="qnty-{{@index}}" name="qnty">
<option value="0">0</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
<option value="7">7</option>
<option value="8">8</option>
<option value="9">9</option>
<option value="10">10</option>
</select>
<button
class="card-button list-btn uppercase flex-grow hover:bg-gradient-to-t md:flex-none ml-2"
onclick="addToCart(this, '{{title}}')"
>
Add to Cart
</button>
</div>
</div>
</div>
{{/each}}
Javascript Function
Only add to the Control, within the tab Script
, not to handlebars template and not in a seperate js file. ai12z runs in the Shadow DOM.
function addToCart(button, title) {
// Find the nearest select[name="qnty"] within the same container as the button
var select = button.parentNode.querySelector('select[name="qnty"]')
var qty = select ? select.value : 0
alert("Added to cart: " + title + ", Quantity: " + qty)
}
Note is the function above uses the button.parentNode
refers to the button's immediate parent, which contains the select.