ONIR CMS
Documentation

Les actions d'un template

Le seul canal du template vers l'hôte : identité, envoi de message, fin, ressources. Ce qui se passe côté serveur, en mock et en production.

Un template ne fait jamais de requête réseau. Il appelle une action ; l'hôte l'exécute (API, base, LLM, médiathèque) puis met à jour le contexte.

Template ──action──▶ Hôte ──HTTP──▶ API / PostgreSQL / LLM / médiathèque
   ▲                    │
   └── nouveau contexte ◀┘

1. Transport et validation

  • Web Component (transport officiel) : ctx.actions est une fonction locale fournie par l'hôte. L'appel est direct, sans postMessage.
  • Iframe (community, hors SDK v1) : le template envoie { action, payload } en postMessage. L'hôte valide chaque action avec parseTemplateAction et rejette tout ce qui n'est pas conforme.

Les actions sont donc validées côté hôte : nom requis, email valide, contenu non vide et ≤ 4000 caractères, URL http(s), hauteur numérique. Une action invalide est ignorée (iframe) ou n'a aucun effet.

2. Catalogue

actions.submitIdentity({ nom, email, contacts?, consentement? })
actions.sendMessage("Bonjour")
actions.attachFile("cin", file)        // champ fichier (ADR-0034)
actions.removeAttachment(attachmentId) // retire une pièce en attente
actions.finish()
actions.request("media" | "modelForm" | "theme" | "messages")
actions.openMedia(url)
actions.resize(hauteur)
ActionRôleChange le contexte
submitIdentitydémarre la conversationphase, messages
sendMessageenvoie un message et récupère la réponsemessages, typing
finishclôt la conversationstatus
requestressource à la demandeévénement resources
openMediaouvre une URLaucun
resizehauteur du cadreaucun
readysignal de montage (iframe)aucun

3. submitIdentity — démarrer

// Identité : démarre la conversation.
async function submit(event) {
  event.preventDefault();
  const data = new FormData(event.currentTarget);
  actions.submitIdentity({
    nom: String(data.get("nom") ?? "").trim(),
    email: String(data.get("email") ?? "").trim(),
    consentement: data.get("consentement") === "on",
  });
}

Payload : { nom, email, contacts?, consentement? }. L'hôte normalise (nom et email trimmés, email en minuscules) et valide (nom non vide, email conforme).

Côté hôte : POST /api/chat/session avec le token du lien, l'identité, le consentement et loadedAt (anti-bot). Le serveur crée ou retrouve le prospect, crée la conversation, applique la politique de confidentialité et renvoie { conversationId, messages }.

  • Réussite : phase = "chat", messages d'accueil remplacés, typing = false.
  • Erreur : la phase ne change pas ; l'hôte affiche un bandeau (« Démarrage impossible » ou « Erreur réseau »).
  • Mock : passage en chat et message d'accueil local, sans réseau.

Désactivez le bouton pendant l'appel : l'hôte ne déduplique pas deux soumissions simultanées.

4. sendMessage — envoyer et traiter

// Message : l'hôte ajoute, affiche "typing", appelle l'API, puis répond.
function send() {
  const texte = draft.trim();
  if (texte === "" || typing) return; // garde anti double-envoi
  actions.sendMessage(texte);
  setDraft("");
}

Payload : { content }, trimmé, non vide, ≤ 4000 caractères.

Côté hôte, dans l'ordre :

  1. Ajout optimiste du message utilisateur, typing = true.
  2. Si la capacité userState et le flag du lien sont actifs : POST /api/chat/state en parallèle pour attacher message.userState (échec sans impact).
  3. POST /api/chat avec { conversationId, content }.
  4. Le serveur persiste le message, borne l'historique à 20 tours, construit le prompt, sélectionne les connaissances (mini-RAG, si la source est active), appelle le LLM, persiste la réponse.
  5. Réponse : { message, connaissances, mediaPending }. Les connaissances ne sont que des références (titre + identifiant), jamais le contenu interne.
  6. Si mediaPending : POST /api/chat/media avec { conversationId, messageId } pour attacher photos/liens — le texte n'attend pas cette recherche.
  7. L'hôte ajoute la réponse, typing = false, puis réassigne le contexte.
SituationRésultat
Message vide ou > 4000 caractèresignoré / 400 côté serveur
Réseau ou LLM en échecmessage utilisateur conservé, pas de réponse, bandeau d'erreur ; vous pouvez réessayer
mediaPendingmédias attachés après coup à la réponse
Mockréponse locale après 500 ms, avec une photo fictive

Garde-fou : if (typing) return ou bouton désactivé pendant l'attente.

5. finish — clôturer

// Fin : clôture (et génération éventuelle côté serveur).
actions.finish();

Côté hôte : POST /api/chat/finish avec { conversationId }. Le serveur clôt la conversation (et génère la sortie prévue). Le contexte passe à status = "finalisee".

  • Erreur : bandeau « Génération impossible pour le moment » ; le statut ne change pas.
  • Mock : status = "finalisee" immédiatement.
  • N'appelez finish qu'une fois : masquez le bouton après succès.

6. attachFile — pièces jointes

Les champs de type file arrivent dans ctx.resources.modelForm avec accept, maxFiles et maxBytes. Le fichier est lu par le template puis envoyé à l'hôte via attachFile(fieldName, file) ; l'hôte valide et stocke. Les pièces en attente sont exposées dans ctx.state.pendingAttachments. Un message peut être envoyé sans texte si des pièces sont en attente. Aucun OCR ni analyse IA n'est effectué.

7. request, openMedia, resize, ready

// Ressource à la demande (facultatif : déjà dans le contexte au chargement).
const media = await actions.request("media");

// Ouvrir un média (nouvel onglet, URL http(s) uniquement).
actions.openMedia(photo.url);

// Ajuster la hauteur (utile en transport iframe).
actions.resize(720);
  • request(capability) : renvoie media, modelForm, theme ou messages, et émet l'événement resources. Utile pour un chargement paresseux ; les données sont déjà dans le contexte initial.
  • openMedia(url) : ouvre un nouvel onglet (noopener) ; seules les URL http(s) sont acceptées.
  • resize(height) : ajuste la hauteur du cadre (0..20000, borné à 200 min par l'hôte). Surtout utile en iframe.
  • ready() : signal de montage, sans effet en Web Component.

8. Événements de l'hôte

const offState = on("state", (state) => { … });      // à chaque contexte
const offRes = on("resources", (value) => { … });     // après request()
offState(); offRes(); // désabonnement

state est émis à chaque mise à jour (message, typing, phase, statut). resources après un request. Les erreurs sont affichées par l'hôte dans son bandeau : prévoyez un état de reprise côté UI (bouton Réessayer).

9. Machine à états

ÉtatActionÉtat suivant
identitysubmitIdentity (succès)chat
chatsendMessagechat + messages + typing
chatfinish (succès)finalisee

10. Mock, aperçu, production

EnvironnementActionsCe qui est réellement exécuté
Harnesssimulées localementrien hors navigateur
Aperçu du liensimuléesle contexte (médias, params, consentement)
Lien publicréellesAPI, LLM, RAG, persistance

Pour observer une action réelle : ouvrez le lien public, DevTools → Réseau, filtrez /api/chat. Vous verrez session, chat, media, finish et leurs réponses.

11. Questions fréquentes

Le message n'est pas envoyé.
Vérifiez conversationId (identité d'abord), le contenu non vide et le statut du lien. Une erreur réseau est affichée par l'hôte.
Deux messages partent pour un seul clic.
Désactivez l'envoi pendant typing : l'hôte ne déduplique pas.
La réponse arrive sans photo.
Vérifiez les sources du lien (photothèque/lien) et les médias actifs du templateKey. mediaPending n'est vrai que si des médias éligibles existent.
Les connaissances ne sont pas visibles.
Elles arrivent dans message.knowledge uniquement si la capacité knowledge est déclarée et la source active sur le lien. Le mock n'en fournit pas.
Puis-je appeler l'API directement ?
Non : le token reste dans l'hôte. Toute action passe par actions.

12. Références

  • Template ↔ hôte — contexte, ressources, création des objets.
  • Contrat des templates visuels — actions et validation.
  • Code : src/components/templates/TemplateHost.tsx, src/domain/template-contract.ts, src/application/use-cases/chat/send-message.ts.