ONIR CMS
Documentation

Créer un template visuel

Interface publique du chat et du formulaire de prospection — contrat onirtech.template-host.v1.

SDK JavaScript des templates — v0.1.3 publié sur npm.

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 official ou verified) 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()
Discussionctx.state.messages + ctx.actions.sendMessage()
Consentementctx.consent (texte + lien de la politique du lien)
Photos et liens proposésctx.resources.media (capacité media), ouverture via ctx.actions.openMedia(url)
Champs du modèle de raisonnementctx.resources.modelForm (capacité modelForm)
Thème du lienctx.theme (capacité theme)
Paramètres du templatectx.params — voir §8
État du visiteur / connaissancesmessage.userState (capacité userState), message.knowledge (capacité knowledge)
Terminer la discussionctx.actions.finish()
Redimensionner l'iframectx.actions.resize(height) (transport iframe)
Technologie front libreVanilla, 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-template

Aprè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"
    }
  ]
}
ChampRègle
schemaVersion / kind1 / "template"
idkebab-case ^[a-z][a-z0-9-]*$, unique (namespace du template)
versionSemVer ; une version publiée ne change plus
publisher.trustLevelofficial | verified | community ; community impose le mode iframe
compatibility.hostProtocolonirtech.template-host.v1
ui.modeweb-component | iframe
ui.entry / ui.elementchemin relatif sûr / nom du Custom Element (kebab-case), effectivement enregistré par l'entrée
ui.integritySRI sha256-… de l'entrée, calculé par pack
ui.shadowDomopen | closed | none
capabilitiessous-ensemble du vocabulaire du §6
networkdomaines externes utilisés par le template (déclaration)
paramsvariables 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.typing indique 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
messagesHistorique de la conversation — ctx.state.messages
mediaPhotos et liens proposés — ctx.resources.media
modelFormChamps du modèle de raisonnement — ctx.resources.modelForm
themeTokens de thème du lien — ctx.theme
userStateÉtat du visiteur par message (capacité ET option du lien) — message.userState
knowledgeConnaissances utilisées par l'assistant (capacité ET source active) — message.knowledge
attachmentsPièces jointes des champs fichier (ADR-0034) — ctx.state.pendingAttachments / message.attachments
dataRessources 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

ActionPayloadContrô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 :

ChampRègle
key / labelclé unique (kebab/snake), libellé 1..80
descriptionaide affichée sous le champ (0..200)
defaultobligatoire (aucun paramètre sans défaut)
maxLength / min / maxbornes selon le type
optionspour select : le défaut doit être l'une des valeurs
TypeContrôle côté lien
stringTexte court (une ligne).
textTexte long (plusieurs lignes).
numberNombre, avec min / max optionnels.
booleanVrai / faux (interrupteur).
colorCouleur #rrggbb (sélecteur).
urlURL http(s).
selectChoix 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

  1. Rendez depuis le contexte à chaque assignation ; ne conservez pas un état local qui ferait autorité sur les messages ou la phase.
  2. Passez par les actions (ctx.actions.*) : pas d'appel direct à l'API du dashboard.
  3. Shadow DOM et CSS scopé : ne stylez pas la page hôte.
  4. Accessibilité : labels explicites, navigation clavier, focus visible, contrastes suffisants.
  5. Mobile : zones tactiles confortables, textes lisibles, mise en page fluide.
  6. Assets dans le bundle : ils sont servis avec la version, en cache immuable.
  7. Premier rendu rapide : différez les chargements lourds ; pour la 3D, respectez les budgets de l'ADR-0021.
  8. Valeurs affichées en texte : n'injectez jamais un contenu du contexte via innerHTML.
  9. Aucun secret dans le bundle : tout est public.
  10. 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 : submitIdentity passe en phase chat, une réponse mock arrive après ~600 ms, finish clô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

  1. Importez l'archive .onirtech-template dans Templates visuels.
  2. Créez ou modifiez un lien de chat, choisissez le template et sa version.
  3. Renseignez les paramètres, puis utilisez Aperçu avec ces valeurs.
  4. 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 chemin ui.entry et relancez pack pour 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.params est absent dans le harness.
Le mock ne fournit pas de paramètres : lisez ctx.params?.clé ?? "défaut".