@callimacus/thamyr-liquid
v4.8.2
Published
Headless Callimacus runtime for Shopify Liquid themes
Readme
@callimacus/thamyr-liquid
Headless Callimacus runtime for Shopify Liquid themes (Online Store 2.0).
The runtime owns transport, round lifecycle, block dispatch, error containment, and the commerce plumbing (currency, URLs, native AJAX cart). The theme owns every element, every class name, all CSS, and all customer-facing copy. The runtime creates no DOM of its own and ships no CSS.
Theme usage (no build step)
Load the self-contained browser bundle — it auto-mounts every
[data-callimacus] element and re-mounts on shopify:section:load:
<script src="https://cdn.jsdelivr.net/npm/@callimacus/thamyr-liquid@4/dist/browser/callimacus-runtime.min.js" defer></script>The floating @4 picks up minor and patch releases automatically. If you prefer
integrity over auto-updates, pin an exact version and add
Subresource Integrity
(integrity="sha384-…" crossorigin="anonymous") — jsdelivr shows the hash on
the file listing. Or skip the CDN entirely: download the file into your theme
as assets/callimacus-runtime.min.js and load it with
{{ 'callimacus-runtime.min.js' | asset_url }} — first-party hosting, no
external script at all (required for Theme Store-listed themes).
Register your renderers from your own file — one entry per block type:
window.callimacusRenderers = window.callimacusRenderers || [];
window.callimacusRenderers.push({
type: 'custom:giftGuide',
render(block, ctx) {
const picks = ctx.picks(block);
if (!picks.length) return null;
const grid = document.createElement('ul');
grid.className = 'gift-guide__grid'; // your class, your CSS
for (const item of picks) grid.append(card(item, ctx));
return grid;
},
});The full walkthrough — mount-point markup, section settings, renderer contract,
and the ctx helpers — lives at
docs.callimacus.ai.
Rounds without a message
Not everything a storefront sends is a typed question. The handle on the mount
element (element.callimacus) carries the TypeScript SDK's calls, with the same
names and options, built by the same helpers:
ask(text)— a typed question.sendInteraction(id, {value, payload, block})— one of the interactions the project declared in its configuration document: a click, a product viewed.openLanding(value?)— the project's custom landing: a round with no message.
The mount supplies the page's language and the visitor's story; pass language
or storyId to override them for one call. Every round runs alike — the same
data-callimacus-state, the same events, one at a time.
A section opens with one round, declared in its markup:
<div data-callimacus data-client-id="…"
data-interaction="productDetail"
data-interaction-value="{{ product.title | escape }}"
data-interaction-payload='{{ product_payload | json | escape }}'>data-interaction (with optional data-interaction-value and a JSON object in
data-interaction-payload) sends a declared interaction; data-landing opens
the custom landing (data-landing="pdp" sends pdp as its value); data-prompt
asks a question. Name several and the first in that order wins, with a warning.
Suggestions
element.callimacus.suggest(prefix) resolves with {suggestions}: the
project's starter questions on an empty or short prefix, autocomplete while the
visitor types — the TypeScript SDK's suggest. It opens no round and changes no
state, so it can run beside one; it connects first when the mount has not yet.
No connection, a timeout (timeoutMs, 4 s by default) or an error all resolve
to no suggestions. Suggestions come back only when the project has them turned
on (Thamyr › Configuration › Suggestions).
input.addEventListener('input', async () => {
const {suggestions} = await root.callimacus.suggest(input.value);
renderChips(suggestions); // yours
});Catalogue lookups without a round
Some answers are plain catalogue lookups, not rounds: nothing to compose,
nothing to generate. ctx.similar(item, {topK}) and ctx.related(item) — the
TypeScript SDK's skesis.similar / skesis.related — read the catalogue
directly, in a few hundred milliseconds, and hand items back shaped like
ctx.picks, so the same card renderer draws them:
const others = await ctx.similar(item, {topK: 12});
const sameModel = others.filter(other => other.fields.title === item.fields.title);ctx.related(item) resolves with {related, siblingColors, usedFallback}:
what the item references, what references it, and its other colours when the
catalogue links them. Both take an item or its id, and read the catalogue in
the locale the storefront's route names (/it-it/ reads it-IT, /it/ reads
it, the root the page's data-language); pass locale to choose another.
They reject with a CallimacusError — lookup_unreachable when the request
failed or ran out of time (timeoutMs, 10 s by default), server_<status>
when Callimacus refused it.
On-screen context
data-ui-observer on the mount element makes every round carry what the
visitor can see — the catalogue items and blocks in view — which the cognitive
flow and on-screen conditions read. It is off by default.
Mark each product element with its item's full id, data-product-id="${item.id}"
(shopify:product:gid://…, never the handle or a numeric Shopify id). While
observing, the runtime marks each rendered block's root element with
data-block-id — the block's own id — unless the renderer set one.
The observer reads every [data-product-id] on the page. If the theme's own
markup already uses that attribute, give the observer a selector of its own:
data-ui-observer="[data-callimacus-product]".
One session across pages
Every ask on a mount continues the same story, so follow-ups carry the
conversation behind them. A storefront reloads on every navigation, though, and
a fresh mount opens a fresh story — unless the theme names the one it kept:
data-story-idon the mount element is the story its first round continues. Empty means none. A story the server no longer has is replaced by a new one.callimacus:story(bubbling, detail{storyId}) fires whenever the server opens a story.element.callimacus.storyIdreads the current one.
Liquid cannot read browser storage, so a few lines of inline script carry it over. Place them after the mount element and before the deferred runtime runs:
<script>
(function () {
var key = 'callimacus:story';
var kept;
try { kept = sessionStorage.getItem(key); } catch (error) {}
document.querySelectorAll('[data-callimacus]').forEach(function (element) {
if (kept && !element.dataset.storyId) element.dataset.storyId = kept;
});
document.addEventListener('callimacus:story', function (event) {
try { sessionStorage.setItem(key, event.detail.storyId); } catch (error) {}
});
})();
</script>To start over — a "new conversation" button — call
element.callimacus.resetStory() and forget the kept id
(sessionStorage.removeItem('callimacus:story')): the next round opens a new
story and announces it. The old story is left as it was.
sessionStorage scopes the conversation to the tab; localStorage would keep
it across visits. Both can throw (private windows, blocked storage), hence the
try: without them the experience still works, one story per page.
Bundler usage
The ESM entry exports the same runtime without the auto-mount side effect:
import {mountAll} from '@callimacus/thamyr-liquid';
mountAll(document);License
Proprietary — licensed for use by Callimacus customers only. See LICENSE.md.
