2. À quoi sert le plein écran s’il existe déjà inline ?
Dans la leçon précédente sur inline, nous sommes déjà convenus que si la tâche est courte et tient en 5–7 éléments ou un seul écran, la carte inline est l’option idéale. Une liste de quelques cadeaux, deux filtres, un ou deux boutons — tout cela vit très bien directement dans le flux de messages.
Mais toute application finit par atteindre un point où « une carte de plus » ne suffit plus :
- il faut collecter de nombreux paramètres (profil du destinataire, contraintes de livraison, moyens de paiement) ;
- il faut un assistant en plusieurs étapes ;
- il y a de grands tableaux, des graphiques, des cartes, de longues descriptions.
Ici, le mode inline commence à être à l’étroit : la largeur est limitée par la colonne du chat, la hauteur aussi, il n’y a pas de navigation, et le chat n’a qu’un seul défilement. C’est précisément pour ces scénarios que le Apps SDK propose le mode plein écran — une interface immersive dans laquelle votre widget occupe la majeure partie de l’écran et peut afficher un layout complexe.
Le deuxième protagoniste du jour — PiP, une petite fenêtre flottante qui vit au-dessus du chat. Ses rôles typiques : statut d’une tâche en arrière-plan, mini-lecteur, minuteur, indicateur de progression. PiP est idéal lorsque quelque chose de long se déroule « en arrière-plan », tandis que l’utilisateur continue à converser avec GPT.
Important à retenir : plein écran et PiP ne remplacent pas inline, ce sont des compléments. On commence par inline, puis on passe en plein écran quand inline devient trop étroit ; on passe en PiP quand tout l’important est déjà lancé et qu’il suffit de garder un œil sur le statut.
3. Fondations techniques : displayMode et basculement des modes
Du point de vue du Apps SDK, votre widget possède un état d’affichage courant — displayMode. Au moment de la rédaction du cours, il existe trois modes principaux : "inline", "fullscreen" et "pip" (picture-in-picture).
L’hôte (ChatGPT) informe votre widget du mode actuel via des données globales dans window.openai et des hooks spécifiques du SDK. Dans un modèle React typique, on a quelque chose comme :
// alias du modèle Apps SDK
const mode = useDisplayMode(); // 'inline' | 'fullscreen' | 'pip'
if (mode === "fullscreen") {
// on rend notre assistant
} else {
// on rend l’UI inline compacte
}
Le SDK fournit aussi la méthode window.openai.requestDisplayMode({ mode }) et/ou le hook useRequestDisplayMode, pour demander à l’hôte de changer de mode. Cette méthode renvoie une promesse avec le mode effectivement appliqué, car la plateforme peut refuser ou ajuster votre requête (par exemple, PiP sur mobile se transforme presque toujours en plein écran).
Schématiquement, on peut représenter le cycle de vie des modes ainsi :
stateDiagram-v2
[*] --> Inline
Inline --> Fullscreen: requestDisplayMode('fullscreen')
Fullscreen --> Inline: requestDisplayMode('inline') / bouton "Retour"
Fullscreen --> PiP: requestDisplayMode('pip')
PiP --> Fullscreen: "Agrandir"
PiP --> Inline: fin de la tâche
Les noms réels et l’ensemble exact des modes peuvent évoluer avec les versions du SDK, donc en production, vérifiez toujours la documentation plutôt que de vous fier au « comme dans le cours ».
4. Premier basculement : créer le bouton « Déployer en plein écran »
Commençons simplement : prenons notre widget inline existant GiftGenius — une App pédagogique des modules précédents qui affiche actuellement 3–5 cartes cadeaux — et ajoutons-y un bouton « Ouvrir la sélection détaillée » pour passer en plein écran.
Supposons que notre modèle contienne deux hooks :
import { useDisplayMode, useRequestDisplayMode } from "@/sdk/display";
export const GiftGeniusWidget: React.FC = () => {
const mode = useDisplayMode();
const requestDisplayMode = useRequestDisplayMode();
if (mode === "fullscreen") {
return <GiftFullscreenWizard />;
}
return (
<InlineGiftPreview
onExpand={async () => {
await requestDisplayMode({ mode: "fullscreen" });
}}
/>
);
};
Ici, InlineGiftPreview est notre UI inline actuelle, et GiftFullscreenWizard est le nouveau composant assistant que nous allons concevoir. Dans le gestionnaire onExpand, nous n’appelons pas seulement requestDisplayMode, mais nous attendons la promesse — ainsi, nous pourrons gérer un refus (par exemple, afficher un message si, pour une raison quelconque, le plein écran est indisponible).
InlineGiftPreview en lui-même est assez simple :
type InlineGiftPreviewProps = {
onExpand: () => void;
};
const InlineGiftPreview: React.FC<InlineGiftPreviewProps> = ({ onExpand }) => {
return (
<div>
<h3>Sélection de cadeaux</h3>
{/* ...cartes de cadeaux... */}
<button onClick={onExpand}>Ouvrir la sélection détaillée</button>
</div>
);
};
Pour l’instant, cela ressemble beaucoup à « ouvrir une modale », mais la différence est que ce n’est pas votre React qui contrôle, c’est l’application hôte ChatGPT, et elle peut afficher un titre, des boutons système « Retour », etc.
5. Concevoir l’assistant plein écran GiftGenius
Concevons maintenant l’assistant plein écran de sélection de cadeau. D’un point de vue UX, il est raisonnable de découper le processus en plusieurs étapes logiques. Par exemple :
- Qui est le destinataire et quelle est l’occasion.
- Budget et type de cadeaux (physiques, expériences, numériques).
- Vérification et confirmation du choix.
En code, on peut refléter cela par une simple machine d’états par étapes :
type WizardStep = "recipient" | "preferences" | "review";
type WizardState = {
step: WizardStep;
recipient?: { ageRange: string; relation: string };
preferences?: { budget: number; categories: string[] };
};
Créons le composant GiftFullscreenWizard, qui stocke cet état dans React et rend l’écran approprié.
const GiftFullscreenWizard: React.FC = () => {
const [state, setState] = useState<WizardState>({ step: "recipient" });
const goNext = (partial: Partial<WizardState>) => {
setState((prev) => ({ ...prev, ...partial }));
};
if (state.step === "recipient") {
return <RecipientStep state={state} onNext={goNext} />;
}
if (state.step === "preferences") {
return <PreferencesStep state={state} onNext={goNext} />;
}
return <ReviewStep state={state} />;
};
Chaque étape est un petit composant avec un formulaire. Par exemple, la première étape :
type StepProps = {
state: WizardState;
onNext: (partial: Partial<WizardState>) => void;
};
const RecipientStep: React.FC<StepProps> = ({ state, onNext }) => {
const [relation, setRelation] = useState(state.recipient?.relation ?? "");
const [ageRange, setAgeRange] = useState(state.recipient?.ageRange ?? "");
return (
<div>
<h2>Pour qui choisissons-nous un cadeau ?</h2>
<input
placeholder="Qui est cette personne pour vous ?"
value={relation}
onChange={(e) => setRelation(e.target.value)}
/>
<input
placeholder="Âge (par exemple, 25–34)"
value={ageRange}
onChange={(e) => setAgeRange(e.target.value)}
/>
<button
onClick={() =>
onNext({
recipient: { relation, ageRange },
step: "preferences",
})
}
>
Suivant
</button>
</div>
);
};
À la deuxième étape, nous recueillons le budget et les catégories ; à la troisième, nous appelons callTool / un outil MCP qui sait déjà sélectionner des cadeaux selon ces paramètres, puis nous affichons les résultats.
L’important, c’est qu’en plein écran nous disposons de l’espace pour :
- une barre de progression ou un stepper ;
- des champs et des aides plus détaillés ;
- des états d’erreur (« quelque chose s’est mal passé, réessayez »).
Recommandation issue des guidelines UX : chaque étape doit rester aussi simple que possible, sans surcharge de champs ; mieux vaut 3–4 étapes claires qu’un formulaire monstre.
6. UX de l’assistant plein écran : progression, erreurs, retour
Se contenter d’afficher un formulaire en plein écran n’est que la moitié du travail. L’utilisateur doit :
- comprendre à quelle étape il se trouve ;
- pouvoir revenir en arrière ;
- voir ce qui se passe pendant les opérations longues.
Un stepper très simple peut être fait visuellement :
const Stepper: React.FC<{ step: WizardStep }> = ({ step }) => {
const index = step === "recipient" ? 1 : step === "preferences" ? 2 : 3;
return <p>Étape {index} sur 3</p>;
};
Et il suffit d’insérer Stepper sur chaque écran. Variante plus avancée : rendre un « escalier » horizontal d’étapes, mais dans le cadre du cours, on ne fera pas d’école de mise en page.
Point important : la gestion des erreurs. Supposons qu’à la dernière étape nous appelions l’outil search_gifts :
const ReviewStep: React.FC<StepProps> = ({ state }) => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleConfirm = async () => {
setLoading(true);
setError(null);
try {
await callTool("search_gifts", {
recipient: state.recipient,
preferences: state.preferences,
});
// Les résultats apparaîtront ensuite dans le chat / le widget
} catch (e) {
setError("Impossible de sélectionner des cadeaux, veuillez réessayer.");
} finally {
setLoading(false);
}
};
return (
<div>
{/* afficher le récapitulatif des paramètres */}
{error && <p style={{ color: "red" }}>{error}</p>}
<button disabled={loading} onClick={handleConfirm}>
{loading ? "Sélection en cours…" : "Confirmer et sélectionner"}
</button>
</div>
);
};
Du point de vue de l’accessibilité, veillez à ce que :
- en plein écran, les gros boutons « Suivant », « Retour » et « Annuler » soient facilement cliquables ;
- le texte ait un contraste adéquat ;
- on puisse parcourir tous les éléments interactifs au clavier (Tab) dans l’ordre.
Si vous le pouvez, ajoutez aria-label pour les contrôles non standard (par exemple, des commutateurs de catégories personnalisés). Même si ce cours n’est pas un examen WCAG, une attention de base à l’a11y vous aidera à passer la revue du Store sans douleur inutile.
Au final, l’assistant plein écran résout les scénarios complexes multi-étapes : il offre de la place pour les formulaires, la progression et les erreurs. Mais la vie de l’application ne s’arrête pas là — de nombreuses tâches se poursuivent « en arrière-plan ». Pour cela, nous avons le deuxième mode : PiP, dont nous allons parler ensuite.
7. Qu’est-ce que le PiP dans l’univers ChatGPT et pourquoi est-il « capricieux »
Nous avons vu comment utiliser le plein écran pour les scénarios complexes. Regardons maintenant le cas opposé — quand tout l’important est déjà lancé et qu’il faut seulement garder le contrôle du progrès. C’est là que PiP entre en scène.
Dans le monde du web, le « picture-in-picture » est généralement associé à une vidéo qui reste dans un coin de l’écran au-dessus du contenu. Dans ChatGPT, PiP est une petite fenêtre flottante du widget, visible lors du défilement du chat et capable d’afficher un statut, une progression ou une UI compacte.
Quelques particularités importantes, issues de la documentation et de l’expérience des early adopters :
- PiP offre très peu d’espace. Ce n’est pas une surface pour des formulaires et des layouts complexes, mais plutôt pour deux ou trois métriques clés et un ou deux boutons.
- Sur desktop, PiP « colle » en haut et reste visible quel que soit le défilement ; sur mobile, il se transforme souvent automatiquement en plein écran.
- La requête requestDisplayMode avec mode "pip" ne garantit pas un vrai PiP. La plateforme peut renvoyer un autre mode (par exemple, plein écran) ou se comporter de façon étrange sur d’anciennes versions du SDK ; vérifiez donc toujours le résultat de la promesse et prévoyez un fallback.
Conclusion UX simple : dans le PiP — uniquement l’essentiel. Un minuteur, un indicateur de livraison, le statut de la tâche, un bouton « Agrandir ». Pas de 12 cases à cocher, pas de tableaux à 10 colonnes, ni « faites-moi encore un café ».
8. GiftGenius + PiP : recherche longue et progression en arrière-plan
Revenons à GiftGenius. Imaginons le scénario : l’utilisateur a parcouru l’assistant plein écran, cliqué sur « Confirmer », et maintenant votre backend lance une sélection assez lourde — peut-être, via un serveur MCP, appelez-vous plusieurs API externes, recalculant des prix, en appliquant des tas de filtres. Cela peut prendre, disons, 10 à 20 secondes.
Du point de vue UX, on ne veut pas garder l’utilisateur 20 secondes en plein écran avec un spinner qui tourne. Mieux vaut :
- Lancer la sélection.
- Réduire l’interface en PiP en affichant la progression.
- Laisser l’utilisateur continuer à discuter (par exemple, poser des questions de précision).
- Après la fin — renvoyer le résultat en inline ou ouvrir un nouveau plein écran avec les cadeaux.
Faisons un hook simple qui gérera ce comportement :
const useLongGiftJob = () => {
const [status, setStatus] = useState<"idle" | "running" | "done">("idle");
const requestDisplayMode = useRequestDisplayMode();
const startJob = async (payload: any) => {
setStatus("running");
const resultMode = await requestDisplayMode({ mode: "pip" });
console.log("Mode effectif :", resultMode.mode);
await callTool("run_gift_job", payload);
setStatus("done");
await requestDisplayMode({ mode: "inline" });
};
return { status, startJob };
};
À présent, dans ReviewStep, au lieu d’appeler directement callTool, nous utilisons ce hook :
const ReviewStep: React.FC<StepProps> = ({ state }) => {
const { status, startJob } = useLongGiftJob();
return (
<div>
{/* ...récapitulatif... */}
<button
disabled={status === "running"}
onClick={() => startJob(state)}
>
{status === "running" ? "Sélection de cadeaux en cours…" : "Lancer la sélection"}
</button>
</div>
);
};
Pour que le statut de la tâche en arrière-plan soit accessible à la fois à l’assistant plein écran et à la fenêtre PiP, dans un vrai code il est judicieux d’extraire useLongGiftJob dans un contexte et de le lire via useLongGiftJobContext. Nous laissons de côté les détails d’implémentation du contexte (Provider, createContext) : l’idée, c’est que l’état du job vit à un seul endroit, et les différentes couches UI s’y abonnent simplement.
Et un composant séparé pour l’affichage PiP :
const GiftPipView: React.FC<{ status: string }> = ({ status }) => {
return (
<div>
<p>GiftGenius fonctionne…</p>
<p>Statut : {status === "running" ? "en cours" : "terminé"}</p>
<button
onClick={() => window.openai.requestDisplayMode({ mode: "fullscreen" })}
>
Agrandir
</button>
</div>
);
};
Dans le widget global, nous adaptons le rendu pour tenir compte aussi du PiP :
const GiftGeniusWidget: React.FC = () => {
const mode = useDisplayMode();
const { status } = useLongGiftJobContext(); // via le contexte, comme discuté plus haut
if (mode === "pip") {
return <GiftPipView status={status} />;
}
if (mode === "fullscreen") {
return <GiftFullscreenWizard />;
}
return <InlineGiftPreview onExpand={/* comme avant */} />;
};
Ce scénario se marie très bien avec les modes vocaux (nous en parlerons dans la leçon sur la voix) : à la voix, nous lançons la sélection, PiP affiche la progression, le chat reste en dessous et continue sa vie.
9. Vidéo + chat : quand plein écran et PiP deviennent un lecteur média
Historiquement, PiP est le plus souvent associé à la vidéo qui reste dans un coin de l’écran au-dessus du contenu. Il est donc logique d’examiner séparément le scénario « video + chat ». Ici non plus, pas de magie : dans la plupart des cas, vous affichez simplement la vidéo en plein écran ou dans la fenêtre PiP. La documentation d’OpenAI cite explicitement les scénarios média comme exemple typique d’utilisation du plein écran et du PiP.
Qu’est-ce que cela peut signifier pour GiftGenius ? Par exemple :
- vous affichez une vidéo promotionnelle du cadeau ;
- un court tutoriel « comment emballer joliment un cadeau » ;
- un test vidéo de plusieurs produits.
En plein écran, on peut rendre un <video> complet avec description et recommandations ; en PiP, garder uniquement le lecteur et, éventuellement, un petit titre.
Un composant d’enveloppe très simple :
const GiftVideoPlayer: React.FC<{ src: string; title: string }> = ({
src,
title,
}) => (
<div>
<h3>{title}</h3>
<video
src={src}
controls
style={{ width: "100%", borderRadius: 8 }}
/>
</div>
);
Dans l’assistant plein écran, nous pouvons proposer à l’utilisateur « Regarder la vidéo de présentation de ce cadeau », puis la réduire en PiP :
const WatchVideoStep: React.FC = () => {
const requestDisplayMode = useRequestDisplayMode();
return (
<div>
<GiftVideoPlayer src="/videos/gift-wrap.mp4" title="Comment emballer un cadeau" />
<button
onClick={() => requestDisplayMode({ mode: "pip" })}
>
Laisser la vidéo dans un coin et revenir au chat
</button>
</div>
);
};
Quelques conseils pratiques pour les scénarios média :
- n’activez pas l’autoplay avec le son — c’est un antipattern UX universel ;
- veillez aux sous-titres et à la possibilité de mettre en pause au clavier (barre d’espace, flèches) ;
- dans la fenêtre PiP, n’essayez pas d’afficher tout le texte annexe, contentez-vous de la vidéo elle-même.
10. État, recréation du widget et particularités mobiles
La question la plus désagréable qu’on pose généralement à ce stade : « L’état React sera-t-il conservé si je passe d’inline à plein écran, puis retour ? »
Réponse courte : n’y comptez pas.
Techniquement, le comportement dépend de la version du SDK et de l’implémentation de l’hôte : dans certains cas, le passage entre modes se fait sans recréer l’iframe ; dans d’autres, le widget est démonté puis remonté. La documentation souligne séparément que la conservation du contexte lors du changement de mode dépend de l’implémentation et de la version du SDK, et n’est pas garantie pour le développeur.
Approche pratique :
- Stockez tout état critique (étape de l’assistant, données saisies, identifiant d’une tâche en arrière-plan) soit :
- dans le backend (via votre serveur MCP et des jetons de session),
- soit dans le contexte ChatGPT (par exemple via des tools qui renvoient « l’état courant du workflow »),
- soit dans les paramètres d’URL / le stockage local, si vous avez une raison sûre de le faire.
- Utilisez l’état React comme cache/couche UI, mais soyez prêts à ce qu’il puisse être remis à zéro lors d’un changement de mode — vous le restaurerez alors depuis une source plus fiable.
La deuxième subtilité concerne le résultat de requestDisplayMode. Comme mentionné, une requête avec mode "pip" peut revenir en "fullscreen", surtout sur mobile, où le vrai PiP peut ne pas être pris en charge ou être automatiquement affiché en plein écran.
Patron typique :
const requestDisplayMode = useRequestDisplayMode();
const openPipSafe = async () => {
const result = await requestDisplayMode({ mode: "pip" });
if (result.mode !== "pip") {
// Fallback : par exemple, afficher un message ou adapter l’UI au plein écran
console.log("PiP indisponible, nous travaillons en mode :", result.mode);
}
};
Ainsi, vous n’aboutirez pas à une situation où vous comptiez sur une petite fenêtre mais avez obtenu une UI plein écran avec des boutons « spécifiques au PiP ». Dans ce mode, cette interface paraîtra étrange.
Enfin, gardez en tête maxHeight et le défilement interne : même en plein écran, l’hôte peut limiter la hauteur du conteneur, et votre tâche est d’organiser le scroll de manière à éviter trois barres de défilement imbriquées.
11. Erreurs courantes avec plein écran et PiP
Erreur n°1 : le plein écran comme mode par défaut.
Certains développeurs voient « fullscreen » et essaient immédiatement de transformer leur App en SPA séparée à l’intérieur du chat. Résultat : à la moindre mention de cadeaux, l’utilisateur est propulsé dans un assistant plein écran alors qu’il voulait juste quelques idées. Les guidelines d’OpenAI recommandent fortement de commencer par inline et de n’étendre au plein écran qu’en cas de besoin objectif.
Erreur n°2 : PiP comme un petit plein écran.
PiP a une surface très limitée, mais on tente parfois d’y tout entasser : onglets, formulaires, filtres. L’utilisateur se retrouve avec une interface microscopique sur laquelle il est impossible de cliquer. La bonne approche : dans PiP, afficher uniquement le statut et un ou deux boutons clés (par exemple, « Agrandir » et « Annuler »).
Erreur n°3 : transitions inexpliquées entre les modes.
Quand le widget se déploie soudainement en plein écran sans texte du GPT ou sans clic explicite, c’est désorientant. Idem pour la réduction automatique en PiP ou le retour en inline. Chaque transition doit être accompagnée d’une brève explication dans un message du modèle : « J’ouvre maintenant l’assistant détaillé » avant le plein écran, « Je réduis la sélection dans une petite fenêtre pendant le calcul » avant le PiP.
Erreur n°4 : ignorer le mobile et les différences de plateforme.
Le développeur teste uniquement sur desktop, où PiP se comporte comme prévu, puis sur mobile tout devient plein écran, la mise en page « bouge », et des boutons se retrouvent hors de la safe area. La documentation prévient explicitement que PiP sur mobile peut être implémenté comme plein écran, et que le comportement peut changer entre versions du SDK ; il est donc obligatoire de tester sur les appareils cibles et de travailler soigneusement avec requestDisplayMode.
Erreur n°5 : croire à la conservation intégrale de l’état lors du changement de mode.
S’appuyer uniquement sur l’état React sans aucune persistance côté serveur conduit à des situations cocasses : l’utilisateur a parcouru deux étapes de l’assistant, a cliqué « Réduire en PiP », et au retour il se retrouve à la première étape avec des champs vides. Il vaut mieux partir du principe qu’au changement de mode, votre composant peut être démonté, et concevoir la gestion d’état en conséquence.
Erreur n°6 : oublier l’accessibilité de l’assistant plein écran.
Un beau formulaire en grand écran n’est pas toujours pratique pour les personnes malvoyantes ou celles qui n’utilisent que le clavier. Texte trop petit, contraste faible, boutons « Suivant » et « Retour » illisibles — causes fréquentes non seulement d’un mauvais UX, mais aussi de problèmes lors de la revue dans le Store. Vérifiez au moins les bases : contraste du texte, taille de police, navigation au Tab, et présence d’étiquettes textuelles claires pour les boutons.
GO TO FULL VERSION