Créer un template visuel
Interface publique du chat et du formulaire de prospection — contrat onirtech.template-host.v1.
Le SDK @onirtech/template-sdk 0.1.3 permet de créer un template en React, Vue ou HTML/CSS/JS sans écrire le squelette du Custom Element : npm install @onirtech/template-sdk. Créez un projet avec npx @onirtech/create-onirtech-template aurora --stack react. Voir le workflow développeur complet.
1. Ce qu'est un template visuel
Un template visuel est un package UI-only : un manifeste (onirtech.template.json) et des assets, rendus chez le visiteur sur la page publique /l/<jeton>. Le dashboard l'installe, le rattache à un lien de chat et le sert avec une version figée.
- Un lien de chat utilise un template et une version.
- Une version publiée est immuable : toute modification produit une nouvelle version.
- Deux transports : Web Component (publisher
officialouverified) et iframe sandboxée (community). - Le template lit un contexte fourni par l'hôte et déclenche des actions ; il ne contient aucun secret.
2. Ce qui est faisable aujourd'hui
| Fonctionnalité | Comment |
|---|---|
| Formulaire d'identification (lien standard) | ctx.state.phase === "identity" + ctx.actions.submitIdentity() |
| Discussion | ctx.state.messages + ctx.actions.sendMessage() |
| Consentement | ctx.consent (texte + lien de la politique du lien) |
| Photos et liens proposés | ctx.resources.media (capacité media), ouverture via ctx.actions.openMedia(url) |
| Champs du modèle de raisonnement | ctx.resources.modelForm (capacité modelForm) |
| Thème du lien | ctx.theme (capacité theme) |
| Paramètres du template | ctx.params — voir §8 |
| État du visiteur / connaissances | message.userState (capacité userState), message.knowledge (capacité knowledge) |
| Terminer la discussion | ctx.actions.finish() |
| Redimensionner l'iframe | ctx.actions.resize(height) (transport iframe) |
| Technologie front libre | Vanilla, framework embarqué… dès lors que le bundle enregistre le Custom Element déclaré. |
Les appels réseau et les écritures passent par les actions du contrat : le template ne dialogue pas directement avec l'API du dashboard.
3. Démarrage en 5 minutes
Le kit fournit un hôte simulé (harness) qui implémente le côté hôte avec des données MOCK : le développement est hors-ligne, sans base ni lien.
# 1. créer un template à partir de la référence
node bin/create-onirtech-template.mjs mon-template
cd mon-template
# 2. développer en local (hôte simulé, données MOCK, hors-ligne)
node ../harness/host.mjs --dir . # → http://localhost:4500
# 3. valider le manifeste, l'entrée et l'intégrité SRI
node ../bin/onirtech-template.mjs validate .
# 4. empaqueter
node ../bin/onirtech-template.mjs pack .
# → release/mon-template-1.0.0.onirtech-templateAprès une modification, rechargez la page du harness. URLs utiles : ?phase=chat (saute le formulaire) et ?consent=0 (sans consentement).
4. Le manifeste
{
"schemaVersion": 1,
"kind": "template",
"id": "aurora",
"name": "Aurora",
"description": "Chat et formulaire clairs, orientés conversion.",
"version": "1.0.0",
"publisher": { "id": "onirtech", "name": "ONIRTECH", "trustLevel": "official" },
"compatibility": { "hostProtocol": "onirtech.template-host.v1" },
"ui": {
"mode": "web-component",
"entry": "./index.js",
"element": "onirtech-template-aurora",
"integrity": "sha256-…",
"shadowDom": "open"
},
"capabilities": ["messages", "theme"],
"network": [],
"params": [
{
"key": "titre",
"label": "Titre",
"description": "Titre affiché en haut du chat",
"type": "string",
"default": "Discutons de votre projet",
"maxLength": 80
},
{
"key": "couleur",
"label": "Couleur principale",
"description": "Couleur des titres et des boutons",
"type": "color",
"default": "#023777"
}
]
}| Champ | Règle |
|---|---|
schemaVersion / kind | 1 / "template" |
id | kebab-case ^[a-z][a-z0-9-]*$, unique (namespace du template) |
version | SemVer ; une version publiée ne change plus |
publisher.trustLevel | official | verified | community ; community impose le mode iframe |
compatibility.hostProtocol | onirtech.template-host.v1 |
ui.mode | web-component | iframe |
ui.entry / ui.element | chemin relatif sûr / nom du Custom Element (kebab-case), effectivement enregistré par l'entrée |
ui.integrity | SRI sha256-… de l'entrée, calculé par pack |
ui.shadowDom | open | closed | none |
capabilities | sous-ensemble du vocabulaire du §6 |
network | domaines externes utilisés par le template (déclaration) |
params | variables paramétrables (§8), optionnel |
5. Le contrat runtime
Le template reçoit un objet contexte en lecture. Il est réassigné à chaque évolution (message, saisie, état) : rendez toujours l'interface à partir du contexte courant.
type ContexteTemplate = {
host: { protocol: "onirtech.template-host.v1"; version: 1 };
link: { type: "standard" | "specifique"; label: string; userStateEnabled: boolean };
locale: string;
brand: { name: string; logoUrl: string | null };
theme: Readonly<Record<string, string>>;
params: Readonly<Record<string, string | number | boolean>>;
capabilities: readonly string[];
consent: { texte: string; lien: string } | null;
state: {
phase: "identity" | "chat";
status: "en_cours" | "finalisee";
messages: { id: string; role: "user" | "assistant"; content: string;
medias: { type: "photo" | "lien"; libelle: string; url: string }[];
at: string; userState?: { label: string; confidence: number };
knowledge?: { ref: string; titre: string }[] }[];
typing: boolean;
};
resources: {
media: { photos: { id: string; libelle: string; url: string }[];
liens: { id: string; libelle: string; url: string }[] };
modelForm: { name: string; label: string; type: string; required: boolean;
options?: string[] }[] | null;
};
};Transport Web Component
L'hôte crée l'élément puis affecte la propriété onirtechContext (et la réaffecte à chaque mise à jour) :
set onirtechContext(ctx) { this.#ctx = ctx; this.#render(); }Transport iframe
L'hôte poste { __onirtech: 1, type: "init", context } dans l'iframe et le template répond par { __onirtech: 1, action, payload }.
Cycle de vie
identity: formulaire (nom, email, consentement) →ctx.actions.submitIdentity().chat: historique et saisie →ctx.actions.sendMessage().ctx.actions.finish()clôt la discussion.ctx.state.typingindique qu'une réponse est en cours.
6. Capacités
Une donnée n'est fournie que si la capacité est déclarée dans le manifeste.
| Capacité | Donnée fournie |
|---|---|
messages | Historique de la conversation — ctx.state.messages |
media | Photos et liens proposés — ctx.resources.media |
modelForm | Champs du modèle de raisonnement — ctx.resources.modelForm |
theme | Tokens de thème du lien — ctx.theme |
userState | État du visiteur par message (capacité ET option du lien) — message.userState |
knowledge | Connaissances utilisées par l'assistant (capacité ET source active) — message.knowledge |
attachments | Pièces jointes des champs fichier (ADR-0034) — ctx.state.pendingAttachments / message.attachments |
data | Ressources de données déclarées par contrat (ADR-0026) — ctx.data.load(contract) |
userState et knowledge exigent en plus l'option correspondante sur le lien (état du visiteur activé / source connaissances cochée).
7. Actions
| Action | Payload | Contrôles |
|---|---|---|
submitIdentity | { nom, email, contacts?, consentement? } | nom requis, email valide |
sendMessage | { content } | non vide, 4 000 caractères maximum |
finish | — | — |
request | { capability } | renvoie media, modelForm ou theme |
openMedia | { url } | http(s) uniquement |
resize | { height } | transport iframe |
8. Variables paramétrables
Le template déclare des variables dans son manifeste ; l'exploitant du lien les renseigne dans Liens → Modifier → Paramètres du template. Elles arrivent résolues dans ctx.params (objet gelé).
// Déclaration (manifeste)
"params": [
{ "key": "titre_de_livre", "label": "Titre du livre",
"description": "Titre affiché sur la couverture de l'album",
"type": "string", "default": "album", "maxLength": 60 }
]
// Lecture (template) — le défaut du manifeste sert de repli
const titre = ctx.params?.titre_de_livre ?? "album";Chaque paramètre déclare :
| Champ | Règle |
|---|---|
key / label | clé unique (kebab/snake), libellé 1..80 |
description | aide affichée sous le champ (0..200) |
default | obligatoire (aucun paramètre sans défaut) |
maxLength / min / max | bornes selon le type |
options | pour select : le défaut doit être l'une des valeurs |
| Type | Contrôle côté lien |
|---|---|
string | Texte court (une ligne). |
text | Texte long (plusieurs lignes). |
number | Nombre, avec min / max optionnels. |
boolean | Vrai / faux (interrupteur). |
color | Couleur #rrggbb (sélecteur). |
url | URL http(s). |
select | Choix parmi les options déclarées. |
- Valeur absente ou invalide ⇒ le défaut est appliqué.
- Les valeurs sont publiques (visibles par le visiteur) : jamais de secret.
- Le bouton « Aperçu avec ces valeurs » teste une valeur sans l'enregistrer.
9. Exemple de template complet
Un template minimal mais complet : formulaire, consentement, chat, et deux paramètres (titre, couleur). Fichiers : onirtech.template.json + index.js.
onirtech.template.json
{
"schemaVersion": 1,
"kind": "template",
"id": "aurora",
"name": "Aurora",
"description": "Exemple complet : formulaire puis chat, deux paramètres.",
"version": "1.0.0",
"publisher": { "id": "onirtech", "name": "ONIRTECH", "trustLevel": "official" },
"compatibility": { "hostProtocol": "onirtech.template-host.v1" },
"ui": {
"mode": "web-component",
"entry": "./index.js",
"element": "onirtech-template-aurora",
"integrity": "sha256-…",
"shadowDom": "open"
},
"capabilities": ["messages", "theme"],
"network": [],
"params": [
{ "key": "titre", "label": "Titre", "description": "Titre du chat",
"type": "string", "default": "Discutons de votre projet", "maxLength": 80 },
{ "key": "couleur", "label": "Couleur principale", "description": "Titres et boutons",
"type": "color", "default": "#023777" }
]
}index.js
const ELEMENT = "onirtech-template-aurora";
function h(tag, props = {}, ...children) {
const node = document.createElement(tag);
for (const [key, value] of Object.entries(props)) {
if (key === "text") node.textContent = value;
else if (key.startsWith("on")) node.addEventListener(key.slice(2).toLowerCase(), value);
else if (value !== null && value !== undefined) node.setAttribute(key, String(value));
}
for (const child of children.flat()) {
if (child === null || child === undefined || child === false) continue;
node.append(child instanceof Node ? child : document.createTextNode(String(child)));
}
return node;
}
class Aurora extends HTMLElement {
#root = this.attachShadow({ mode: "open" });
#ctx = null;
set onirtechContext(ctx) { this.#ctx = ctx; this.#render(); }
get onirtechContext() { return this.#ctx; }
connectedCallback() { this.#render(); }
#render() {
const ctx = this.#ctx;
if (!ctx) return;
const titre = ctx.params?.titre ?? "Discutons de votre projet";
const couleur = ctx.params?.couleur ?? "#023777";
this.#root.replaceChildren(
this.#style(couleur),
h("h2", { text: titre }),
ctx.state.phase === "identity" ? this.#identite(ctx) : this.#chat(ctx),
);
}
#style(couleur) {
const style = document.createElement("style");
style.textContent = `
:host { display:block; font-family:system-ui,sans-serif; color:#0b2247; }
.wrap { max-width:32rem; margin:0 auto; padding:1.25rem; }
form, .composer { display:flex; gap:.5rem; flex-wrap:wrap; margin-top:.75rem; }
input { flex:1 1 12rem; padding:.6rem .7rem; border:1px solid #cfd4de;
border-radius:.5rem; font:inherit; }
button { background:${couleur}; color:#fff; border:0; border-radius:.5rem;
padding:.6rem 1rem; font:inherit; cursor:pointer; }
ul { list-style:none; padding:0; display:flex; flex-direction:column; gap:.5rem; }
li { background:#f1f5f9; border-radius:.5rem; padding:.55rem .75rem; }
li.user { background:#e2e8f0; }
`;
return style;
}
#identite(ctx) {
const nom = h("input", { type: "text", placeholder: "Votre nom", "aria-label": "Nom" });
const email = h("input", { type: "email", placeholder: "vous@exemple.mg", "aria-label": "Email" });
const consentement = h("input", { type: "checkbox" });
const form = h("form", {
onsubmit: (event) => {
event.preventDefault();
if (nom.value.trim() === "" || email.value.trim() === "") return;
ctx.actions.submitIdentity({
nom: nom.value.trim(),
email: email.value.trim(),
consentement: consentement.checked,
});
},
},
nom,
email,
ctx.consent
? h("label", { class: "consent" }, consentement, " ", ctx.consent.texte)
: null,
h("button", { type: "submit", text: "Démarrer la discussion" }),
);
return h("section", { class: "wrap" }, form);
}
#chat(ctx) {
const liste = h("ul", {},
...ctx.state.messages.map((message) =>
h("li", { class: message.role }, message.content)));
const champ = h("input", { type: "text", placeholder: "Votre message…", "aria-label": "Message" });
const envoyer = () => {
if (champ.value.trim() === "") return;
ctx.actions.sendMessage(champ.value.trim());
champ.value = "";
};
return h("section", { class: "wrap" },
liste,
ctx.state.typing ? h("p", { text: "…" }) : null,
h("div", { class: "composer" },
champ,
h("button", { type: "button", text: "Envoyer", onclick: envoyer })),
);
}
}
if (!customElements.get(ELEMENT)) customElements.define(ELEMENT, Aurora);Le kit fournit aussi un template de référence minimal (template/) et un exemple avancé 3D (template-immersive/), utile pour les assets lourds, la 3D et les paramètres.
10. Bonnes pratiques
- Rendez depuis le contexte à chaque assignation ; ne conservez pas un état local qui ferait autorité sur les messages ou la phase.
- Passez par les actions (
ctx.actions.*) : pas d'appel direct à l'API du dashboard. - Shadow DOM et CSS scopé : ne stylez pas la page hôte.
- Accessibilité : labels explicites, navigation clavier, focus visible, contrastes suffisants.
- Mobile : zones tactiles confortables, textes lisibles, mise en page fluide.
- Assets dans le bundle : ils sont servis avec la version, en cache immuable.
- Premier rendu rapide : différez les chargements lourds ; pour la 3D, respectez les budgets de l'ADR-0021.
- Valeurs affichées en texte : n'injectez jamais un contenu du contexte via
innerHTML. - Aucun secret dans le bundle : tout est public.
- Vérifiez les deux phases (identité et chat), le consentement, les messages vides et les textes longs avant de packager.
11. Tester et déboguer
- Le harness simule les actions :
submitIdentitypasse en phase chat, une réponse mock arrive après ~600 ms,finishclôt la discussion. - Ouvrez les DevTools et inspectez le shadow root de votre élément pour vérifier le rendu.
- Si l'élément ne monte pas : vérifiez le nom d'élément, le chemin d'entrée et relancez
pack(l'intégrité SRI doit être à jour). - Si une donnée manque : vérifiez qu'elle est bien déclarée dans
capabilities.
12. Publier et rattacher
- Importez l'archive
.onirtech-templatedans Templates visuels. - Créez ou modifiez un lien de chat, choisissez le template et sa version.
- Renseignez les paramètres, puis utilisez Aperçu avec ces valeurs.
- Pour une nouvelle version, incrémentez
version, repackagez, importez et sélectionnez la nouvelle version sur le lien.
13. Références
- Norme
docs/templates-visuels/norme-v1.md(contrat complet). - ADR-0019 (packages UI-only), ADR-0020 (capacités cœur), ADR-0023 (variables paramétrables).
- SDK des templates (ADR-0024) — workflow React, Vue et HTML/CSS/JS.
- Actions d'un template — identité, envoi, fin et traitement côté hôte.
- Kit :
template-kit/(scaffolder, harness, starter, exemple immersif).
14. Questions fréquentes
- Mon Custom Element ne s'affiche pas.
- Vérifiez
ui.element, le cheminui.entryet relancezpackpour recalculer l'intégrité. - Le contexte est vide.
- La donnée n'est fournie que si la capacité est déclarée dans le manifeste.
- Mes modifications n'apparaissent pas.
- Une version est immuable : incrémentez
version, repackagez et importez. ctx.paramsest absent dans le harness.- Le mock ne fournit pas de paramètres : lisez
ctx.params?.clé ?? "défaut".