2. Perché serve il fullscreen se esiste l’inline?
Nella lezione precedente su inline abbiamo già concordato: se il compito è breve e rientra in 5–7 elementi o in una sola schermata, la scheda inline è l’opzione ideale. Un elenco di alcuni regali, un paio di filtri, uno‑due pulsanti — tutto questo vive benissimo direttamente nel flusso dei messaggi.
Ma per qualsiasi applicazione arriva il momento in cui “un’altra scheda” non basta più:
- bisogna raccogliere molti parametri (profilo del destinatario, vincoli di consegna, metodi di pagamento);
- serve un wizard composto da più passaggi;
- ci sono grandi tabelle, grafici, mappe, descrizioni lunghe.
Inline qui inizia a “soffrire”: la larghezza è limitata dalla colonna della chat, l’altezza anche, non c’è navigazione e lo scroll della chat è unico. Proprio per questi scenari nell’Apps SDK esiste la modalità fullscreen: un’interfaccia “immersiva” in cui il vostro widget occupa gran parte dello schermo e può mostrare layout complessi.
Il secondo protagonista di oggi è il PiP, una piccola finestra flottante che vive sopra la chat. I suoi ruoli tipici: stato di un’attività in background, mini player, timer, indicatore di avanzamento. Il PiP è ideale quando qualcosa di duraturo procede “in background” mentre l’utente continua a parlare con GPT.
È importante ricordare: sia fullscreen sia PiP non sostituiscono l’inline, ma sono un’estensione. Si parte dall’inline e si passa al fullscreen quando l’inline diventa stretto; si va in PiP quando tutto il “bello” è già avviato e serve solo “tenere d’occhio” lo stato.
3. Fondamenta tecniche: displayMode e cambio di modalità
Dal punto di vista dell’Apps SDK, il vostro widget ha un stato di visualizzazione corrente — displayMode. Al momento della stesura del corso ci sono tre modalità principali: "inline", "fullscreen" e "pip" (picture‑in‑picture).
L’host (ChatGPT) comunica al vostro widget la modalità corrente tramite i dati globali in window.openai e speciali hook dell’SDK. In un tipico template React c’è qualcosa del genere:
// alias dal template dell'Apps SDK
const mode = useDisplayMode(); // 'inline' | 'fullscreen' | 'pip'
if (mode === "fullscreen") {
// renderizziamo il nostro wizard
} else {
// renderizziamo la UI inline compatta
}
L’SDK fornisce anche il metodo window.openai.requestDisplayMode({ mode }) e/o l’hook useRequestDisplayMode per chiedere all’host di cambiare modalità. Questo metodo restituisce una Promise con la modalità effettivamente impostata, perché la piattaforma può rifiutare o correggere la vostra richiesta (ad esempio, su mobile il PiP quasi sempre diventa fullscreen).
Schematicamente il ciclo di vita delle modalità si può rappresentare così:
stateDiagram-v2
[*] --> Inline
Inline --> Fullscreen: requestDisplayMode('fullscreen')
Fullscreen --> Inline: requestDisplayMode('inline') / pulsante "Indietro"
Fullscreen --> PiP: requestDisplayMode('pip')
PiP --> Fullscreen: "Espandi"
PiP --> Inline: fine del task
I nomi reali e l’insieme esatto delle modalità possono cambiare con le versioni dell’SDK, quindi in produzione conviene sempre ricontrollare la documentazione e non basarsi su “come era nel corso”.
4. Primo switch: aggiungiamo il pulsante “Espandi a schermo intero”
Partiamo dal semplice: prendiamo il nostro widget inline GiftGenius — un’App didattica dei moduli precedenti che ora mostra 3–5 schede di regali — e aggiungiamo il pulsante “Apri selezione dettagliata” per passare al fullscreen.
Supponiamo che nel nostro template ci siano due hook:
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" });
}}
/>
);
};
Qui InlineGiftPreview è la nostra attuale UI inline, mentre GiftFullscreenWizard è il nuovo componente‑wizard che andremo ora a progettare. Nell’handler di onExpand non ci limitiamo a chiamare requestDisplayMode, ma attendiamo la Promise: in questo modo potremo reagire in seguito a un eventuale rifiuto (ad esempio, mostrare un messaggio se per qualche motivo il fullscreen non è disponibile).
Il componente InlineGiftPreview è piuttosto semplice:
type InlineGiftPreviewProps = {
onExpand: () => void;
};
const InlineGiftPreview: React.FC<InlineGiftPreviewProps> = ({ onExpand }) => {
return (
<div>
<h3>Selezione di regali</h3>
{/* ...schede dei regali... */}
<button onClick={onExpand}>Apri selezione dettagliata</button>
</div>
);
};
Per ora sembra “aprire una modale”, ma la differenza è che a controllare non è il vostro React, bensì l’applicazione host ChatGPT, che può mostrare un titolo, pulsanti di sistema “Indietro” ecc.
5. Progettiamo il wizard fullscreen di GiftGenius
Ora progettiamo il wizard fullscreen per la selezione del regalo. Dal punto di vista UX è ragionevole dividere il processo in alcuni passaggi logici. Ad esempio:
- Chi è il destinatario del regalo e qual è l’occasione.
- Budget e tipo di regali (fisici, esperienze, digitali).
- Verifica e conferma della scelta.
Nel codice si può riflettere tutto con una semplice macchina a stati per i passaggi:
type WizardStep = "recipient" | "preferences" | "review";
type WizardState = {
step: WizardStep;
recipient?: { ageRange: string; relation: string };
preferences?: { budget: number; categories: string[] };
};
Creiamo il componente GiftFullscreenWizard, che conserva questo stato in React e renderizza la schermata necessaria.
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} />;
};
Ogni passaggio è un piccolo componente con un form. Ad esempio, il primo passaggio:
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>Per chi stiamo scegliendo il regalo?</h2>
<input
placeholder="Chi è per te?"
value={relation}
onChange={(e) => setRelation(e.target.value)}
/>
<input
placeholder="Età (ad esempio, 25–34)"
value={ageRange}
onChange={(e) => setAgeRange(e.target.value)}
/>
<button
onClick={() =>
onNext({
recipient: { relation, ageRange },
step: "preferences",
})
}
>
Avanti
</button>
</div>
);
};
Nel secondo passaggio raccogliamo budget e categorie; nel terzo invochiamo lo strumento callTool / MCP che già sa selezionare i regali in base a questi parametri e mostriamo i risultati.
È importante che nella schermata fullscreen abbiamo spazio per:
- una barra di progresso o uno stepper;
- campi e suggerimenti più estesi;
- stati di errore (“qualcosa è andato storto, riprova”).
Raccomandazione dalle linee guida UX: ogni passaggio deve rimanere il più semplice possibile, senza sovraccarico di campi; meglio 3–4 step chiari che un unico mostro‑formularo.
6. UX del wizard fullscreen: progresso, errori, ritorno
Mostrare un form a tutto schermo è solo metà dell’opera. L’utente deve:
- capire a che passaggio si trova;
- avere la possibilità di tornare indietro;
- vedere cosa succede durante le operazioni lunghe.
Uno stepper semplicissimo si può realizzare solo visivamente:
const Stepper: React.FC<{ step: WizardStep }> = ({ step }) => {
const index = step === "recipient" ? 1 : step === "preferences" ? 2 : 3;
return <p>Passo {index} di 3</p>;
};
E semplicemente inserire Stepper in ogni schermata. Una variante più avanzata è renderizzare una “scala” orizzontale dei passaggi, ma nell’ambito del corso non faremo una scuola di front‑end.
Un punto importante è la gestione degli errori. Supponiamo che all’ultimo passaggio invochiamo lo strumento 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,
});
// I risultati poi appariranno nella chat / nel widget
} catch (e) {
setError("Impossibile selezionare i regali, riprova.");
} finally {
setLoading(false);
}
};
return (
<div>
{/* mostrare il riepilogo dei parametri */}
{error && <p style={{ color: "red" }}>{error}</p>}
<button disabled={loading} onClick={handleConfirm}>
{loading ? "Stiamo selezionando…" : "Conferma e seleziona"}
</button>
</div>
);
};
Dal punto di vista dell’accessibilità bisogna assicurarsi che:
- nel fullscreen i pulsanti “Avanti”, “Indietro” e “Annulla” siano facilmente cliccabili;
- il testo abbia un contrasto adeguato;
- con Tab si possano attraversare tutti gli elementi interattivi in ordine.
Se possibile, aggiungete aria-label per i controlli non standard (ad esempio, switch personalizzati delle categorie). Anche se il corso non è un esame WCAG, un’attenzione di base all’a11y vi aiuterà a passare la revisione dello Store senza inutili dolori.
In definitiva, il wizard fullscreen risolve i flussi multistep complessi: offre spazio per form, progresso ed errori. Ma la vita dell’app non finisce qui — molte attività continuano “in background”. Per questo abbiamo la seconda modalità — il PiP, di cui parleremo ora.
7. Che cos’è il PiP nel mondo ChatGPT e perché è “capriccioso”
Abbiamo visto come usare il fullscreen per scenari complessi. Guardiamo ora al caso opposto: quando tutto l’importante è già avviato e serve solo “tenere sotto controllo” l’avanzamento. Qui entra in gioco il PiP.
Nel mondo web “picture‑in‑picture” di solito si associa al video che resta nell’angolo dello schermo sopra il contenuto. In ChatGPT il PiP è una piccola finestra flottante del widget che rimane visibile durante lo scroll della chat e può mostrare stato, progresso o una UI compatta.
Alcune caratteristiche importanti, da documentazione ed esperienza degli early adopter:
- Il PiP ha pochissimo spazio. Non è un’area per form e layout complessi, ma piuttosto per due‑tre metriche chiave e uno‑due pulsanti.
- Su desktop il PiP si “ancora” in alto e resta visibile con qualsiasi scroll; su mobile invece spesso si trasforma automaticamente in fullscreen.
- Una richiesta requestDisplayMode con mode "pip" non garantisce un vero PiP. La piattaforma può restituire un’altra modalità (ad esempio fullscreen) o comportarsi in modo strano su versioni vecchie dell’SDK, quindi verificate sempre il risultato della Promise e prevedete un fallback.
Ne consegue un semplice principio UX: nel PiP — solo l’essenziale. Timer, indicatore di consegna, stato del task, pulsante “Espandi”. Niente 12 checkbox, tabelle a 10 colonne o “fammi anche il caffè”.
8. GiftGenius + PiP: ricerca lunga e progresso in background
Torniamo a GiftGenius. Immaginiamo lo scenario: l’utente ha completato il wizard fullscreen, ha premuto “Conferma” e ora il vostro backend avvia una selezione piuttosto pesante — magari tramite un server MCP chiamate diverse API esterne, ricalcolate prezzi, applicate un sacco di filtri. Può richiedere, poniamo, 10–20 secondi.
Dal punto di vista UX non vogliamo tenere l’utente 20 secondi in fullscreen con uno spinner che gira. Meglio:
- avviare la selezione;
- ridurre l’interfaccia in PiP, mostrando l’avanzamento;
- dare modo all’utente di continuare la chat (ad esempio, porre domande di chiarimento);
- al termine — restituire il risultato inline o aprire un nuovo fullscreen con i regali.
Facciamo un semplice hook che gestirà questo comportamento:
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("Modalità effettiva:", resultMode.mode);
await callTool("run_gift_job", payload);
setStatus("done");
await requestDisplayMode({ mode: "inline" });
};
return { status, startJob };
};
Ora, in ReviewStep, invece di chiamare callTool direttamente, usiamo questo hook:
const ReviewStep: React.FC<StepProps> = ({ state }) => {
const { status, startJob } = useLongGiftJob();
return (
<div>
{/* ...riepilogo... */}
<button
disabled={status === "running"}
onClick={() => startJob(state)}
>
{status === "running" ? "Selezioniamo i regali…" : "Avvia selezione"}
</button>
</div>
);
};
Perché lo stato del task in background sia accessibile sia al wizard fullscreen sia alla finestra PiP, nel codice reale ha senso spostare useLongGiftJob in un contesto e leggerlo tramite useLongGiftJobContext. Tralasciamo i dettagli dell’implementazione del contesto (Provider, createContext): è importante che il job‑state viva in un unico posto e che i diversi layer UI si limitino a sottoscriverlo.
E un componente separato per la vista PiP:
const GiftPipView: React.FC<{ status: string }> = ({ status }) => {
return (
<div>
<p>GiftGenius è in esecuzione…</p>
<p>Stato: {status === "running" ? "in corso" : "pronto"}</p>
<button
onClick={() => window.openai.requestDisplayMode({ mode: "fullscreen" })}
>
Espandi
</button>
</div>
);
};
Nel widget principale sostituiremo il render in modo da tenere conto anche del PiP:
const GiftGeniusWidget: React.FC = () => {
const mode = useDisplayMode();
const { status } = useLongGiftJobContext(); // tramite contesto, come discusso sopra
if (mode === "pip") {
return <GiftPipView status={status} />;
}
if (mode === "fullscreen") {
return <GiftFullscreenWizard />;
}
return <InlineGiftPreview onExpand={/* come prima */} />;
};
Questo scenario si combina perfettamente con le modalità vocali (ne parleremo nella lezione su voice): con la voce avviamo la selezione, il PiP mostra il progresso, la chat resta in basso e continua a vivere la sua vita.
9. Video + chat: quando fullscreen e PiP diventano un media player
Storicamente il PiP è più spesso associato al video che resta nell’angolo dello schermo sopra il contenuto. È quindi logico analizzare a parte lo scenario “video + chat”. Anche qui non c’è alcuna magia: nella maggior parte dei casi mostrate semplicemente un video in fullscreen o nella finestra PiP. La documentazione OpenAI riporta esplicitamente gli scenari media come esempio tipico d’uso di fullscreen e PiP.
Cosa può significare questo per GiftGenius? Ad esempio:
- mostrate un video promozionale del regalo;
- un breve tutorial “come impacchettare un regalo con stile”;
- una video‑recensione di diversi prodotti.
In fullscreen potete renderizzare un <video> completo con descrizione e raccomandazioni; nel PiP — lasciare solo il player e, forse, un piccolo titolo.
Un componente wrapper semplicissimo:
const GiftVideoPlayer: React.FC<{ src: string; title: string }> = ({
src,
title,
}) => (
<div>
<h3>{title}</h3>
<video
src={src}
controls
style={{ width: "100%", borderRadius: 8 }}
/>
</div>
);
Nel wizard fullscreen possiamo proporre all’utente “Guarda la video‑recensione di questo regalo” e poi ridurla in PiP:
const WatchVideoStep: React.FC = () => {
const requestDisplayMode = useRequestDisplayMode();
return (
<div>
<GiftVideoPlayer src="/videos/gift-wrap.mp4" title="Come impacchettare un regalo" />
<button
onClick={() => requestDisplayMode({ mode: "pip" })}
>
Lascia il video in un angolo e torna alla chat
</button>
</div>
);
};
Alcuni consigli pratici per gli scenari media:
- non attivate l’autoplay con audio — è un antipattern UX universale;
- curate i sottotitoli e la possibilità di mettere in pausa da tastiera (barra spaziatrice, frecce);
- nella finestra PiP non cercate di mostrare tutto il testo associato, limitatevi al solo video.
10. Stato, rimontaggio del widget e peculiarità mobile
La domanda più spiacevole che di solito si fa a questo punto: “Lo stato di React si conserva se passo da inline a fullscreen e indietro?”
Risposta breve: non contateci.
Tecnicamente il comportamento dipende dalla versione dell’SDK e dall’implementazione dell’host: in alcuni casi il passaggio tra modalità avviene senza ricreare l’iframe, in altri — il widget viene smontato e montato di nuovo. La documentazione sottolinea che la conservazione del contesto al cambio di modalità dipende dalla specifica implementazione dell’SDK e dalla sua versione e non è garantita per lo sviluppatore.
Approccio pratico:
- Conservate tutto lo stato critico (passo del wizard, dati inseriti, identificatore del task in background) o:
- nel backend (tramite il vostro server MCP e i token di sessione),
- oppure nel contesto di ChatGPT (ad esempio tramite tools che restituiscono “lo stato corrente del workflow”),
- oppure nei parametri URL/local storage, se ci sono motivazioni di sicurezza per farlo.
- Usate lo state di React come cache/strato UI, ma siate pronti al fatto che al cambio di modalità possa azzerarsi — in tal caso lo ripristinate da una fonte più affidabile.
La seconda finezza riguarda il risultato di requestDisplayMode. Come già detto, una richiesta con mode "pip" può tornare come "fullscreen", soprattutto su mobile, dove il vero PiP potrebbe non essere supportato o essere automaticamente espanso a tutto schermo.
Pattern tipico:
const requestDisplayMode = useRequestDisplayMode();
const openPipSafe = async () => {
const result = await requestDisplayMode({ mode: "pip" });
if (result.mode !== "pip") {
// Fallback: ad es., mostrare un messaggio o adattare la UI al fullscreen
console.log("PiP non disponibile, lavoriamo in modalità:", result.mode);
}
};
Così non vi ritroverete nella situazione in cui contavate su una finestrella e avete ottenuto una UI a schermo intero con controlli “specifici del PiP”. In quel contesto un’interfaccia del genere apparirebbe strana.
Infine, ricordate maxHeight e lo scroll interno: anche in fullscreen l’host può limitare l’altezza del contenitore e sta a voi organizzare lo scroll in modo da evitare tre barre di scorrimento annidate.
11. Errori tipici nell’uso di fullscreen e PiP
Errore n. 1: Fullscreen come modalità predefinita.
Alcuni sviluppatori vedono la parola “fullscreen” e cercano subito di trasformare la propria App in una SPA separata dentro la chat. Il risultato: a ogni menzione dei regali — l’utente finisce nel wizard a schermo intero, mentre voleva solo un paio di idee. Le linee guida OpenAI raccomandano con insistenza di partire dall’inline e di espandere a fullscreen solo quando c’è un’esigenza oggettiva.
Errore n. 2: PiP come un piccolo fullscreen.
Il PiP ha un’area molto limitata, ma a volte ci si prova a infilare tutto: tab, form, filtri. L’utente riceve un’interfaccia microscopica in cui è impossibile cliccare. L’approccio corretto: nel PiP mostrare solo lo stato e uno‑due pulsanti chiave (ad esempio, “Espandi” e “Annulla”).
Errore n. 3: Transizioni tra modalità non spiegate.
Quando un widget si espande all’improvviso in fullscreen senza un testo da parte di GPT o senza un click esplicito dell’utente, si crea disorientamento. Vale lo stesso per l’auto‑riduzione in PiP o il ritorno in inline. Ogni transizione va accompagnata da una breve spiegazione nel messaggio del modello: “Ora apro il wizard dettagliato” prima del fullscreen; “Ridurrò la selezione in una finestrella mentre viene calcolata” prima del PiP.
Errore n. 4: Ignorare il mobile e le differenze tra piattaforme.
Lo sviluppatore testa solo su desktop, dove il PiP si comporta come previsto, e poi su mobile tutto diventa fullscreen, il layout “salta” e i pulsanti finiscono fuori dalla safe‑area. La documentazione avvisa chiaramente che su mobile il PiP può essere implementato come fullscreen e che il comportamento può cambiare tra versioni dell’SDK; dunque test sui dispositivi target e uso attento di requestDisplayMode sono obbligatori.
Errore n. 5: Fede cieca nella conservazione dello stato al cambio di modalità.
Affidarsi solo allo state di React senza alcun supporto server/persistente porta a situazioni imbarazzanti: l’utente ha completato due passaggi del wizard, ha premuto “Riduci in PiP” e, al ritorno, si ritrova al primo passo con i campi vuoti. Meglio considerare che al cambio di modalità il vostro componente possa essere smontato e progettare la gestione dello stato tenendo conto di questo rischio.
Errore n. 6: Accessibilità del wizard fullscreen dimenticata.
Un form bello su grande schermo non è sempre confortevole per chi ha una vista ridotta o usa solo la tastiera. Testo troppo piccolo, scarso contrasto, pulsanti “Avanti” e “Indietro” illeggibili — cause frequenti non solo di una cattiva UX, ma anche di problemi in revisione sullo Store. Conviene verificare almeno le basi: contrasto del testo, dimensione del font, funzionamento della navigazione con Tab e presenza di etichette testuali chiare per i pulsanti.
GO TO FULL VERSION