1. Qu’est-ce que le bac à sable et pourquoi votre widget est en cage
Quand ChatGPT affiche votre widget, il ne le rend pas comme un <iframe src="https://votre-site"> ordinaire. Le widget s’exécute dans un « bac à sable » géré — un iframe isolé avec un origin distinct et des réglages de sécurité stricts.
Techniquement, cela ressemble à ceci :
flowchart TD
User["Utilisateur dans ChatGPT"]
Chat["UI ChatGPT + modèle"]
Iframe["Votre widget
sandboxed iframe"]
MCP["Votre MCP / backend"]
User --> Chat
Chat -->|appel d’outil| MCP
MCP -->|structuredContent + _meta| Chat
Chat -->|window.openai.*| Iframe
Iframe -->|callTool / follow-up| Chat
Chat --> MCP
Votre code s’exécute uniquement à l’intérieur de cet iframe, et l’accès au reste du monde passe par une API étroitement contrôlée fournie par l’hôte (ChatGPT). Le widget ne doit pas :
- casser ChatGPT lui-même (DOM, styles, performances) ;
- violer la confidentialité de l’utilisateur ;
- effectuer des accès réseau incontrôlés.
Il en découle des limitations clés du bac à sable.
Isolation du DOM et de l’origine
Le widget vit sur un domaine spécial de bac à sable (par exemple, https://sandbox-apps.oaiusercontent.com), avec l’attribut sandbox sur l’iframe. Cela signifie que :
- vous ne pouvez pas toucher au window.parent ni au document de ChatGPT — vous obtiendrez un SecurityError ;
- les mécanismes cross-domain comme postMessage sont contrôlés par l’hôte ;
- toute tentative de « réparer l’interface de ChatGPT avec un petit CSS » est vouée à l’échec.
Réseau et contraintes CSP
Le navigateur et la politique CSP de l’hôte limitent l’accès réseau de votre widget :
- la méthode fetch n’a accès qu’aux domaines de la liste blanche, qui doivent passer en revue ;
- vous déclarez explicitement, via openai/widgetCSP dans les réponses MCP, les domaines accessibles depuis le widget ; sinon les requêtes échouent ;
- la voie recommandée pour tout ce qui est sérieux est de ne pas appeler le réseau depuis le widget, mais de passer par le backend via des outils MCP et callTool (nous détaillerons cela dans le module 4).
Concrètement : considérez le widget comme une fine couche UI. Il parle à ChatGPT et à votre serveur via des canaux strictement définis, et non comme un SPA classique libre sur Internet.
Stockages et ressources
Les stockages locaux (localStorage, sessionStorage) vous sont accessibles, mais pas les cookies. Tenez-en compte lors du développement. La mémoire et le CPU sont limités : si vous décidez de recalculer tous les nombres premiers jusqu’à un milliard dans le widget, l’hôte a parfaitement le droit de tuer votre iframe.
Conclusion importante : pas de calculs lourds ni de « caches » de longue durée dans le widget. La logique complexe doit rester côté serveur, pas dans un composant React.
2. window.openai : un pont entre le widget et ChatGPT
Pour que le widget puisse connaître quoi que ce soit (résultats d’outils, mode d’affichage, locale, état), ChatGPT intègre à l’initialisation dans la fenêtre de l’iframe un objet global — window.openai.
Ce n’est ni votre code ni un paquet npm, mais un host object fourni par la plateforme d’IA. Sous le capot, il s’appuie sur des événements et des messages entre l’hôte et l’iframe, mais vous n’avez presque pas à vous en soucier. Il est important de garder quelques points en tête.
Qui crée window.openai et quand
window.openai n’apparaît que :
- dans l’iframe que ChatGPT a créé pour votre widget ;
- quand le template HTML est servi avec le bon mimeType (text/html+skybridge) et a passé toutes les validations.
Vous avez déjà vu ce type dans le module sur HelloWorld App — c’est bien celui que la page du widget renvoie au lieu d’un text/html normal.
Si vous ouvrez simplement la page du widget directement dans le navigateur, alors :
console.log(window.openai); // undefined
et c’est normal. Par conséquent, dans le code du widget, il est toujours utile de vérifier que l’objet existe si vous comptez sur un mode « standalone » pour le développement local ou Storybook.
Exemple primitif (non définitif, juste pour illustration) :
if (typeof window !== "undefined" && (window as any).openai) {
console.log("We are inside ChatGPT sandbox!");
}
Asynchronisme de l’initialisation
Sous le capot, ChatGPT met à jour window.openai au fil de l’arrivée de nouvelles données (nouveau toolOutput, changement de displayMode, etc.), en utilisant l’événement interne openai:set_globals.
Autrement dit, ses « valeurs » ne sont pas statiques : le modèle d’IA peut appeler un outil MCP, le backend renverra un nouveau structuredContent, et window.openai.toolOutput changera directement sous votre composant React.
Deux recommandations en découlent :
- Ne faites pas de snapshots « à l’aveugle » du type const toolOutput = window.openai.toolOutput une seule fois au démarrage en pensant qu’il est éternel. Un même widget peut être réutilisé par ChatGPT.
- Utilisez une couche de hooks (voir plus loin) qui sait s’abonner aux changements.
3. Anatomie de window.openai : données, API et contexte
La documentation officielle propose un tableau assez compact des champs et méthodes de window.openai. Voici une version plus « humaine ».
Champs et méthodes principaux
window.openai = {
// State & data
toolInput, // Paramètres JSON que l’IA a transmis à votre outil MCP
toolOutput, // Paramètres JSON que votre outil MCP a renvoyés à l’IA
toolResponseMetadata, // Réponse de l’outil MCP : partie _meta: {...}
widgetState, // Permet de lire l’état enregistré du widget
setWidgetState, // Permet d’enregistrer ici l’état de votre widget
// Runtime APIs
callTool, // Permet d’appeler un outil MCP
sendFollowUpMessage, // Envoyer discrètement un message à l’IA dans le chat : elle commencera à répondre.
requestDisplayMode, // Basculer le widget dans un autre mode : fullscreen, pip, inline
requestModal, // Transformer le widget en fenêtre modale.
requestClose, // Ferme le widget. Fermer la modale la retransforme en widget.
requestCheckout, // Ouvre une modale de paiement. Le serveur doit implémenter ACP
notifyIntrinsicHeight, // Notifier un changement de hauteur intrinsèque du widget
openExternal, // Ouvrir un lien dans une nouvelle fenêtre.
// Context
theme, // Thème sombre ou clair
displayMode, // Mode d’affichage actuel du widget, peut différer de requestDisplayMode
maxHeight, // Hauteur maximale autorisée du widget
safeArea, // « zone sûre de rendu » — pertinent pour les téléphones avec encoches
view,
userAgent, // userAgent du navigateur
locale // locale du navigateur
}
La même chose sous forme de tableau :
| Catégorie | Propriété / méthode | À quoi ça sert |
|---|---|---|
| State & data | |
Arguments avec lesquels l’outil a été appelé. Lecture seule. |
| State & data | |
Votre structuredContent depuis la réponse MCP. Ce que voit le widget et le modèle. |
| State & data | |
_meta de la réponse. Visible uniquement par le widget, le modèle ne le lit pas. |
| State & data | |
Instantané d’état UI que ChatGPT conserve entre les rendus du widget. |
| State & data | |
Enregistrer un nouvel instantané de widgetState de façon synchrone. |
| Function | |
Appeler un outil MCP depuis le widget. |
| Function | |
Demander à ChatGPT d’envoyer un message dans le chat au nom du widget. Il commencera à répondre. |
| Function | |
Demander à l’hôte inline / fullscreen / pip. |
| Function | |
Demander l’ouverture d’une fenêtre modale. |
| Function | |
Signaler que la hauteur du contenu a changé. |
| Function | |
Ouvre une boîte de dialogue de paiement via le protocole ACP. |
| Function | |
Ouvrir un lien externe dans le navigateur de l’utilisateur. |
| Context | |
Signaux d’environnement : thème, mode, hauteur disponible, locale, etc. |
Inutile de tout mémoriser immédiatement — considérez ce tableau comme une « carte ». Maintenant, voyons cela non pas comme un « référentiel », mais de manière pragmatique.
toolInput et toolOutput : d’où viennent les données
Quand le modèle décide d’appeler votre outil, il forme des arguments JSON. Ces arguments :
- arrivent sur le serveur MCP en tant que input dans le handler ;
- arrivent simultanément dans window.openai.toolInput dans le widget.
Après l’exécution de l’outil, le serveur renvoie :
- structuredContent — des données structurées pour l’UI ;
- _meta — des données privées pour le widget uniquement ;
- content — du texte pour le modèle lui-même afin qu’il puisse « expliquer » à l’utilisateur ce qui s’est passé.
structuredContent devient window.openai.toolOutput, et _meta devient window.openai.toolResponseMetadata.
Mini-exemple (JS vanilla, sans React) :
const root = document.getElementById("root");
// On peut utiliser l’opérateur nullish en toute sécurité
const gifts = window.openai.toolOutput?.gifts ?? [];
root.textContent = `Cadeaux trouvés: ${gifts.length}`;
widgetState et setWidgetState : la mémoire du widget
widgetState, c’est ce que la plateforme accepte de mémoriser sur votre UI entre les rendus et même entre des tours de dialogue.
Exemples naturels pour widgetState :
- le cadeau sélectionné ;
- le tri en cours (par prix / popularité) ;
- le numéro de page dans une liste.
Pas naturels :
- la réponse brute d’une API tierce ;
- une image en base64 ;
- des jetons secrets.
Deux points à garder à l’esprit :
- widgetState est stocké et transmis au modèle avec le contexte, donc on n’y met rien de sensible.
- Le volume est limité (environ 4 000 tokens), donc on n’en fait pas une mini-base de données.
Exemple minimal (sans hooks, en JS vanilla) :
const current = window.openai.widgetState ?? { selectedGiftId: null };
function selectGift(id) {
window.openai.setWidgetState({ ...current, selectedGiftId: id });
}
En pratique, nous envelopperons cela dans des hooks React.
Runtime API : callTool, sendFollowUpMessage et consorts
Ces méthodes permettent au widget non seulement de « se dessiner », mais aussi d’interagir avec le dialogue et le serveur.
Quelques scénarios typiques :
- callTool("search_gifts", { budget: 50 }) — l’utilisateur clique sur le bouton « Modifier le budget », vous appelez le serveur et mettez à jour l’UI ;
- sendFollowUpMessage({ prompt: "Montre d’autres idées plus chères" }) — au lieu de demander à l’utilisateur de saisir du texte, vous ajoutez un bouton de relance (follow-up) qui crée un nouveau message dans le chat ;
- requestDisplayMode({ mode: "fullscreen" }) — si le mode inline devient étroit, le widget peut poliment demander à ChatGPT de passer en plein écran ;
- openExternal({ href: "https://myshop.com/checkout?giftId=123" }) — envoi de l’utilisateur vers un site externe (checkout, profil, etc.) via un canal approuvé.
Tout passe « par les fils » via ChatGPT, et non directement sur Internet.
Contexte d’exécution : thème, mode, hauteur, locale
Des champs comme theme, displayMode, maxHeight, locale donnent une idée de l’environnement dans lequel vit le widget.
Par exemple :
const theme = window.openai.theme; // "light" ou "dark"
const mode = window.openai.displayMode; // "inline" | "fullscreen" | "pip"
const maxH = window.openai.maxHeight; // hauteur disponible
const locale = window.openai.locale; // "en-US", "de-DE", ...
Avec ces signaux, vous pouvez :
- adapter les couleurs et espacements au thème ;
- modifier le layout selon le mode (inline vs fullscreen) ;
- localiser les libellés UI selon la langue de l’utilisateur (un module entier y sera consacré).
La plateforme vous donne des signaux sur l’espace disponible, le thème et la locale. Il est judicieux de les utiliser via useOpenAIGlobal, useDisplayMode, useMaxHeight et d’autres hooks, afin que le widget paraisse « natif » dans ChatGPT.
4. Hooks au-dessus de window.openai : ne touchez pas l’objet global à la main
Un accès direct à window.openai est pratique pour un prototype, mais transforme vite le code en bouillie : abonnements aux événements, vérifications de undefined, wrappers répétitifs. C’est pourquoi, dans le template Next.js pour l’Apps SDK, il existe un ensemble de hooks React qui cachent ces détails et rendent le tout réactif.
L’index typique des hooks ressemble à ceci :
// app/hooks/openai/index.ts
export { useCallTool } from "./use-call-tool";
export { useSendMessage } from "./use-send-message";
export { useOpenExternal } from "./use-open-external";
export { useRequestDisplayMode, useRequestModal, useRequestClose } from "./use-request-display-mode";
export { useRequestCheckout } from "./use-request-checkout";
// State hooks
export { useDisplayMode } from "./use-display-mode";
export { useWidgetProps } from "./use-widget-props";
export { useWidgetState } from "./use-widget-state";
export { useOpenAIGlobal } from "./use-openai-global";
export { useMaxHeight } from "./use-max-height";
export { useIsChatGptApp } from "./use-is-chatgpt-app";
Les noms et le chemin exact peuvent légèrement différer dans votre template, mais l’idée est la même partout : au lieu de window.openai.*, vous utilisez des hooks. Voyons les principaux.
useWidgetProps : entrées et sorties de l’outil
useWidgetProps renvoie généralement un objet avec les données nécessaires au widget : toolInput, toolOutput, toolResponseMetadata et parfois des drapeaux additionnels comme isLoading.
Exemple :
import { useWidgetProps } from "../hooks/openai";
type Gift = { id: string; title: string; price: number };
export function GiftList() {
const { toolOutput } = useWidgetProps<{ gifts: Gift[] }>();
const gifts = toolOutput?.gifts ?? [];
if (!gifts.length) {
return <div>Pas encore d’options de cadeaux.</div>;
}
return (
<ul>
{gifts.map((g) => (
<li key={g.id}>{g.title} — ${g.price}</li>
))}
</ul>
);
}
Aucun window.openai dans le code du composant — et c’est très bien.
useWidgetState : une « enveloppe réactive » autour de widgetState
useWidgetState permet de manipuler widgetState comme un état React classique : vous obtenez [state, setState], et le hook synchronise en interne avec window.openai.widgetState et setWidgetState.
Exemple :
import { useWidgetState } from "../hooks/openai";
type UiState = { selectedGiftId: string | null };
export function SelectedGiftIndicator() {
const [uiState, setUiState] = useWidgetState<UiState>(() => ({
selectedGiftId: null,
}));
if (!uiState?.selectedGiftId) {
return <div>Aucun cadeau n’a encore été sélectionné.</div>;
}
return (
<div>
Vous avez choisi un cadeau avec id={uiState.selectedGiftId}
<button onClick={() => setUiState({ selectedGiftId: null })}>
Réinitialiser
</button>
</div>
);
}
Après le clic, setUiState mettra non seulement à jour l’état React, mais enregistrera aussi le nouvel état côté ChatGPT.
useOpenAIGlobal : accès à n’importe quel champ de window.openai
Si vous avez besoin d’un champ global (par exemple le thème ou le mode), utilisez le hook universel useOpenAIGlobal(key). Il s’abonne à l’événement openai:set_globals et renvoie une valeur toujours à jour.
Exemple :
import { useOpenAIGlobal } from "../hooks/openai";
export function ThemeAwareBlock() {
const theme = useOpenAIGlobal<"light" | "dark">("theme");
const background = theme === "dark" ? "#222" : "#fff";
const color = theme === "dark" ? "#fff" : "#000";
return <div style={{ background, color }}>Je respecte le thème de ChatGPT</div>;
}
useCallTool, useSendMessage, useOpenExternal et autres
- useCallTool(name) — renvoie une fonction qui appelle l’outil MCP portant ce nom. C’est un wrapper autour de callTool.
- useSendMessage() — enveloppe sendFollowUpMessage pour permettre au widget d’initier des messages.
- useOpenExternal() — helper pratique autour de openExternal({ href }).
- useRequestDisplayMode() et useRequestModal() — wrappers pour demander un changement de mode / l’ouverture d’une modale.
Exemple de mini-widget GiftGenius, qui utilise presque tout à la fois :
import {
useWidgetProps,
useWidgetState,
useCallTool,
useSendMessage,
useOpenExternal,
} from "../hooks/openai";
type Gift = { id: string; title: string; url: string; price: number };
export function GiftWidget() {
const { toolOutput } = useWidgetProps<{ gifts: Gift[] }>();
const gifts = toolOutput?.gifts ?? [];
const [ui, setUi] = useWidgetState<{ selectedId: string | null }>(() => ({
selectedId: null,
}));
const callSearch = useCallTool("search_gifts");
const sendMessage = useSendMessage();
const openExternal = useOpenExternal();
if (!gifts.length) {
return <div>Pas encore d’idées. Essayez de demander à GPT de mettre à jour les résultats.</div>;
}
return (
<div>
{gifts.map((g) => (
<button
key={g.id}
style={{
display: "block",
fontWeight: ui?.selectedId === g.id ? "bold" : "normal",
}}
onClick={() => setUi({ selectedId: g.id })}
>
{g.title} — ${g.price}
</button>
))}
<div style={{ marginTop: 12 }}>
<button
onClick={() =>
sendMessage({ prompt: "Montre des cadeaux plus chers que les actuels." })
}
>
Demander d’autres idées
</button>
<button
onClick={async () => {
await callSearch({ budget: 200 });
}}
>
Mettre à jour avec un budget de $200
</button>
{ui?.selectedId && (
<button
onClick={() =>
openExternal({
href: `https://giftgenius.example.com/checkout?id=${ui.selectedId}`,
})
}
>
Passer à l’achat
</button>
)}
</div>
</div>
);
}
Cette page est encore brute (nous améliorerons l’UX, la gestion d’erreurs, etc. dans les modules suivants), mais illustre déjà l’approche : aucun appel direct à window.openai, uniquement des hooks.
5. Pratique : explorer le bac à sable et window.openai
Pour ressentir ce qu’est « un widget qui n’est pas un site classique », il est utile de faire quelques exercices.
Exercice : « Tester l’environnement »
Prenez votre app/page.tsx actuel dans le widget et ajoutez au premier rendu un petit effet :
import { useEffect } from "react";
import { useIsChatGptApp } from "../hooks/openai";
export default function Root() {
const isChatGpt = useIsChatGptApp();
useEffect(() => {
if (typeof window !== "undefined") {
console.log("window.origin =", window.origin);
console.log("window.openai =", (window as any).openai);
}
}, []);
return (
<main>
<h1>GiftGenius widget</h1>
<p>Lancé à l’intérieur de ChatGPT: {String(isChatGpt)}</p>
</main>
);
}
Ouvrez les DevTools : soit directement dans la fenêtre de ChatGPT (via le viewer intégré du tunnel, s’il le permet), soit dans votre navigateur local en ouvrant la page directement. Comparez dans les deux cas :
- au lancement dans un navigateur classique, isChatGptApp vaudra false, et window.openai sera probablement undefined ;
- au lancement via ChatGPT, vous verrez un objet avec les champs toolInput, toolOutput, theme, etc.
C’est une bonne intuition : le même code React se comporte différemment selon l’environnement, et c’est précisément à cela que servent les hooks.
Exercice : « Afficher tout ce que fournit la plateforme »
Ajoutez un composant temporaire pour le debug :
import { useWidgetProps, useOpenAIGlobal } from "../hooks/openai";
export function DebugPanel() {
const { toolInput, toolOutput, toolResponseMetadata } = useWidgetProps();
const theme = useOpenAIGlobal("theme");
const displayMode = useOpenAIGlobal("displayMode");
return (
<pre style={{ fontSize: 10, maxHeight: 200, overflow: "auto" }}>
{JSON.stringify(
{ toolInput, toolOutput, toolResponseMetadata, theme, displayMode },
null,
2
)}
</pre>
);
}
Et insérez temporairement <DebugPanel /> sous l’UI principal. Vous verrez clairement :
- quels champs arrivent exactement depuis le MCP dans toolOutput ;
- ce qui vit dans _meta (par exemple locale, userLocation, etc.) ;
- comment displayMode change quand vous déployez le widget.
Vous pourrez ensuite retirer ce composant ou le laisser activable via un drapeau du type DEBUG_WIDGET.
6. Relations : ChatGPT ↔ widget ↔ MCP/serveur
Pour ne pas considérer le widget comme « l’acteur principal » du système, il est utile de fixer à nouveau les rôles.
- L’utilisateur écrit un message : « Trouve un cadeau pour ma copine, budget 50$ ».
- Le modèle ChatGPT décide d’appeler votre outil MCP search_gifts avec les arguments { recipient: "girlfriend", budget: 50 }.
- Le serveur MCP exécute la logique métier et renvoie :
- content avec une brève description pour le modèle ;
- structuredContent avec un tableau de cadeaux ;
- _meta avec des détails techniques (par exemple source et devise).
- ChatGPT :
- affiche un message texte à l’utilisateur (« J’ai trouvé quelques options… ») ;
- crée un iframe-widget et y transmet structuredContent et _meta via window.openai.toolOutput et toolResponseMetadata.
- Votre widget :
- rend l’UI à partir de toolOutput ;
- lors des interactions, appelle callTool ou envoie un follow-up.
- Le modèle décide ensuite quoi faire avec les résultats de ces actions.
Tout cela mène à une idée importante : le widget n’est jamais le seul maître du processus. C’est une couche UI qui vit dans un écosystème composé du modèle et du serveur MCP. Les tâches complexes (authentification, accès à des données privées, logique métier sérieuse) doivent rester côté serveur. Le widget est responsable d’une interface conviviale et d’une communication soignée avec l’utilisateur.
7. Politiques et règles du jeu dans le bac à sable
Toute cette architecture avec un iframe isolé et window.openai existe pour des raisons de sécurité et de confidentialité. Les guides officiels d’OpenAI soulignent plusieurs principes.
Premièrement, minimisation des données. Vous ne devez pas essayer, via le widget, d’extraire le maximum de PII (personally identifiable information) et de les envoyer chez vous. Tout ce qui est vraiment nécessaire doit être clairement décrit dans les outils, et le modèle comme la couche de sécurité surveilleront attentivement ces appels.
Deuxièmement, interdiction du tracking caché et du fingerprinting. Il est interdit de construire un système « d’espionnage » de l’appareil utilisateur, de collecter des empreintes du navigateur, et de contourner les restrictions. Des paramètres tels que userAgent, userLocation, etc. — sont des indices pour l’UX, pas pour l’authentification ou l’identification.
Troisièmement, tout ce que vous placez dans structuredContent, _meta, widgetState, peut d’une manière ou d’une autre être vu par l’utilisateur ou par un reviewer du Store. Donc :
- aucune clé API, jeton, mot de passe ou secret admin ne doit s’y trouver ;
- l’état du widget doit être conçu de sorte que l’utilisateur ne soit pas surpris en le voyant dans des logs ou en debug.
Quatrièmement, appels réseau. Les requêtes directes depuis le widget vers des API tierces ne sont autorisées que vers une liste très limitée de domaines et pour des scénarios non sensibles. Dès qu’il est question d’argent, de comptes, de données privées — tout doit passer par MCP/backend.
8. Erreurs courantes dans le bac à sable et avec window.openai
Erreur n° 1 : penser que le widget est « un site classique dans un iframe ».
Par habitude, les débutants essaient d’accéder à window.parent, de modifier les styles de ChatGPT, ou d’utiliser localStorage comme d’habitude. Dans le bac à sable, cela ne marche pas ou mal : origin différent, storage isolé, accès au DOM bloqué. Il faut accepter que vous vivez dans un environnement géré et que vous communiquez avec l’hôte uniquement via window.openai et des hooks.
Erreur n° 2 : toucher à window.openai partout directement.
Du code du type window.openai.toolOutput dans dix composants mène à une app difficile à déboguer. Vous devez alors gérer vous-même les événements, l’asynchronisme et les vérifications de undefined. Il est bien plus fiable d’utiliser useWidgetProps, useWidgetState, useOpenAIGlobal et d’autres hooks qui enveloppent déjà openai:set_globals et synchronisent l’état.
Erreur n° 3 : tout stocker dans widgetState (surtout des secrets).
La tentation est grande d’y mettre « au cas où » un gros objet avec des résultats d’API, voire un jeton d’accès. Résultat : le contexte gonfle, le modèle fonctionne moins bien, et vous enfreignez des règles de sécurité de base. widgetState doit rester petit, contenir uniquement des signaux d’UI, et jamais de données confidentielles.
Erreur n° 4 : tenter d’accéder à Internet directement depuis le widget.
Des appels fetch("https://api.superbank.com/...") depuis le bac à sable rencontreront presque assurément du CORS, et même si vous configurez tout à la perfection, ce sera non sécurisé et mal contrôlé. Tout ce qui touche aux comptes réels, à l’argent et aux données personnelles doit être implémenté comme des outils MCP et appelé via callTool ou via la partie serveur.
Erreur n° 5 : compter sur la stabilité de window.openai hors de ChatGPT.
Certains essaient de lancer le widget en SPA séparé et n’ajoutent pas de vérification indiquant que window.openai peut être undefined. En dev, cela finit en crash « Cannot read properties of undefined ». Utilisez useIsChatGptApp, des vérifications typeof window !== "undefined" et un UI de secours quand le widget n’existe pas en tant que tel.
Erreur n° 6 : ignorer le contexte (theme, displayMode, maxHeight, locale).
On peut certes fixer une hauteur 2000px, forcer le thème sombre et viser le desktop — mais l’expérience sera étrange. La plateforme fournit des signaux sur l’espace, le thème et la locale — utilisez-les via useOpenAIGlobal, useDisplayMode, useMaxHeight, etc., pour que le widget paraisse « chez lui » dans ChatGPT.
Erreur n° 7 : tenter de « contourner » la politique via des scripts tiers.
La tentation peut être de charger un tracker, un bundle JS tiers, ou d’exécuter du code depuis un domaine externe « en douce ». Le bac à sable et les politiques CSP sont faits pour empêcher cela : les scripts tiers sont bloqués, et les tentatives de contournement mènent droit au rejet de votre App dans le Store.
GO TO FULL VERSION