CodeGym /Corsi /ChatGPT Apps /Gestione dell'aspetto: dis...

Gestione dell'aspetto: displayMode, maxHeight, borders, theme, layout

ChatGPT Apps
Livello 3 , Lezione 1
Disponibile

1. Perché gestire l’aspetto

Al momento il tuo widget probabilmente assomiglia a «un normale componente React»: un div, un elenco di elementi, un paio di pulsanti. Nel web tradizionale spesso basta. In ChatGPT però c’è una particolarità: la tua UI vive all’interno della chat, dove l’utente ha già molto contesto visivo — messaggi, altre App, interfaccia vocale, oltre a vincoli sulle dimensioni del contenitore.

È importante ricordare due cose.

Primo, il widget ha una modalità di visualizzazione (displayMode): inline, fullscreen, a volte PiP. Dalla modalità dipendono l’area disponibile, il comportamento dello scroll e le aspettative dell’utente.

Secondo, la piattaforma comunica al widget i vincoli di altezza (maxHeight) e il tema (theme). Se li ignori e disegni qualcosa grande quanto Notion dentro un singolo messaggio, la chat si trasforma in un “buco nero” in cui tutto scompare in un enorme iframe. OpenAI raccomanda esplicitamente di mantenere la UI concisa e rispettare colori/typography di sistema.

Lo scenario tipico di GiftGenius mostra bene come funziona nella pratica. L’utente chiede: «Trova un regalo per un amico entro $50». ChatGPT avvia GiftGenius, che in modalità inline mostra schede compatte dei regali e un paio di pulsanti. L’utente fa clic su «Dettagli» — il widget richiede il fullscreen e lì mostra filtri, descrizioni dettagliate, recensioni. Durante il checkout, si può mostrare un piccolo PiP/modale con lo stato «Elaboriamo l’ordine…», senza coprire l’intera chat.

Il nostro obiettivo in questa lezione è imparare a:

  • capire qual è l’attuale displayMode e comportarsi di conseguenza;
  • cambiare modalità su richiesta (inline ↔ fullscreen, a volte PiP);
  • rispettare maxHeight ed evitare il “doppio scroll”;
  • adattare gli stili al tema chiaro/scuro e alla larghezza dello schermo;
  • creare un layout che sembri “nativo” dentro ChatGPT.

2. Modalità displayMode: inline, fullscreen, PiP

Cominciamo dai concetti. displayMode è lo stato del contenitore del tuo widget in ChatGPT. Arriva dalla piattaforma (tramite window.openai.displayMode o l’hook useDisplayMode) e può assumere valori come "inline", "fullscreen", "pip".

Inline

Inline è la modalità predefinita. Il widget viene inserito nel flusso dei messaggi come un altro “blocco” tra le risposte testuali. La larghezza è limitata dalla colonna della chat (su desktop ~700–800px, su telefono è la larghezza dello schermo), e l’altezza è dinamica ma non infinita.

Inline è ideale per:

  • rappresentazioni brevi e autosufficienti: schede regalo, elenchi di opzioni, riepiloghi di ricerca;
  • una o due azioni: «Scegli», «Annulla», «Mostra altro».

Per GiftGenius questa è la modalità principale: l’utente scrive una richiesta e tu mostri 3–5 schede con pulsanti senza occupare tutto lo schermo.

Fullscreen (Canvas)

Fullscreen (o canvas) è la modalità in cui il widget occupa gran parte dell’area visibile. La chat non scompare: la riga di input è ancora disponibile, ma l’attenzione principale è sulla tua UI.

Ha senso abilitare il fullscreen quando:

  • ci sono molti campi di input o un wizard complesso (checkout, filtri avanzati, impostazioni);
  • devi mostrare grandi tabelle, mappe, comparazioni di decine di elementi;
  • inline non basta più e comincia a sembrare un mini-Excel alto 700px.

In GiftGenius il fullscreen serve per fornire filtri completi, ordinamento, descrizioni dettagliate, magari più tab.

PiP / Modal

PiP (picture-in-picture) e le modali sono piccole finestre “flottanti” sopra il contenuto principale. Nelle implementazioni attuali dell’Apps SDK il PiP spesso è realizzato o come modalità speciale di displayMode, oppure come finestra modale tramite requestModal().

Sono utili quando:

  • devi mostrare lo stato di un processo lungo (elaborazione ordine, rendering video);
  • devi chiedere qualcosa di piccolo senza interrompere il flusso principale (una conferma rapida);
  • vuoi permettere all’utente di “tenere il widget a vista” continuando a chattare.

In GiftGenius può essere un piccolo pannello «Stiamo completando l’ordine… 30%» con il pulsante «Annulla».

Piccolo confronto

Tabella per supporto visivo:

Modalità Dove vive Casi tipici Limitazioni
inline
nel flusso dei messaggi Liste, schede, uno-due pulsanti Altezza limitata, larghezza ridotta
fullscreen
sopra la chat / a lato Wizard, form complesse, tabelle Richiede un layout e una navigazione ben progettati
PiP / modal livello flottante Stato, mini-form, video Spazio molto ridotto: tutto deve essere grande e semplice

È importante non considerare il fullscreen come la “vera applicazione” e l’inline come un “preview”. È la stessa App, solo in pose diverse.

3. Hook per lavorare con la modalità: useDisplayMode, useRequestDisplayMode, useRequestModal

Ora che abbiamo chiarito che cosa siano inline/fullscreen/PiP dal punto di vista UX, vediamo come usarli nel codice tramite gli hook dell’Apps SDK.

Invece di leggere direttamente window.openai.displayMode, usiamo un hook del template, già sottoscritto ai cambiamenti e che ti risparmia danze rituali con gli eventi dell’SDK. Un’interfaccia tipica è questa:

// pseudotipi; verificate i nomi reali nel template
type DisplayMode = 'inline' | 'fullscreen' | 'pip';

function useDisplayMode() {
  // restituisce la modalità corrente
  return { displayMode: 'inline' as DisplayMode };
}

function useRequestDisplayMode() {
  // funzione per richiedere il cambio di modalità
  return {
    requestDisplayMode: (mode: DisplayMode) => {
      /* chiama window.openai.requestDisplayMode */
    },
  };
}

Creiamo un componente semplice che mostra la modalità corrente e offre un pulsante «Espandi / Riduci»:

import { useDisplayMode, useRequestDisplayMode } from '@/apps-sdk';

export function DisplayModeDebug() {
  const { displayMode } = useDisplayMode();
  const { requestDisplayMode } = useRequestDisplayMode();

  const toggle = () => {
    requestDisplayMode(displayMode === 'inline' ? 'fullscreen' : 'inline');
  };

  return (
    <div className="text-xs text-gray-500 flex gap-2 items-center">
      <span>Modalità: {displayMode}</span>
      <button onClick={toggle} className="underline">
        Alterna
      </button>
    </div>
  );
}

Nelle app reali di solito nascondi questi elementi di “debug”, ma in Dev Mode un componente del genere aiuta a capire come il widget si comporta quando cambi modalità.

Inline vs fullscreen con sottocomponenti differenti

Un errore comune è cercare di gestire tutte le modalità con lo stesso layout e riempire il JSX di if (displayMode === ...). È più comprensibile separare le viste:

import { useDisplayMode } from '@/apps-sdk';
import { GiftListInline } from './GiftListInline';
import { GiftListFullscreen } from './GiftListFullscreen';

export function GiftWidget() {
  const { displayMode } = useDisplayMode();

  if (displayMode === 'fullscreen') {
    return <GiftListFullscreen />;
  }

  return <GiftListInline />;
}

Così il codice si legge come “se fullscreen — ecco il wizard complesso, altrimenti — la versione inline compatta”. Ogni sottocomponente si può stilizzare separatamente in base ai propri vincoli. Questo approccio è consigliato: separare le modalità in sottocomponenti invece di un enorme if/else in un unico componente.

Modali: useRequestModal

Se il template offre l’hook useRequestModal, la sua interfaccia è di solito simile:

const { requestModal } = useRequestModal();
// requestModal({ title }) o qualcosa di simile.

Le modali somigliano in parte al fullscreen, ma non lo sostituiscono: il fullscreen è per scenari grandi, la modale per un passo singolo e breve (confermare un’azione, inserire un codice sconto, ecc.).

4. Controllo delle dimensioni: maxHeight, scroll e notifyIntrinsicHeight()

Il secondo asse importante è l’altezza. La piattaforma dice al widget: “Ecco l’altezza massima disponibile”. Questo limite si può leggere in window.openai.maxHeight o tramite l’hook useMaxHeight.

Perché non si può semplicemente impostare «height: 5000px»

Se ignori maxHeight e imposti un’altezza fissa enorme, ChatGPT sarà costretto a tagliare il tuo contenuto. Oppure darà all’utente lo scroll doppio: esterno — della chat, interno — del tuo widget. È un UX spiacevole: l’utente deve indovinare dove scorrere per raggiungere il pulsante desiderato.

La strategia corretta è questa:

  1. Leggere il limite maxHeight.
  2. Costruire il layout in modo che lo scroll principale rimanga della chat (soprattutto in inline).
  3. In fullscreen ci si può permettere un po’ di scroll interno, ma con moderazione.

useMaxHeight e limitazione del container

Scriviamo un semplice wrapper che imposta il massimo in altezza per il contenitore radice:

import { useMaxHeight } from '@/apps-sdk';

export function WidgetContainer(props: { children: React.ReactNode }) {
  const { maxHeight } = useMaxHeight(); // ad esempio, 600

  return (
    <div
      style={{ maxHeight }}
      className="overflow-y-auto p-4 bg-background border border-border rounded-xl"
    >
      {props.children}
    </div>
  );
}

Qui rispettiamo onestamente l’altezza massima e abilitiamo lo scroll verticale all’interno del contenitore, ma con misura. In pratica, in inline è meglio evitare molto scroll interno e, invece di liste enormi, mostrare una parte dei dati con un pulsante «Mostra altro» oppure proporre il fullscreen.

Altezza dinamica e notifyIntrinsicHeight()

Un altro aspetto: il tuo contenuto può cambiare dimensione nel tempo. Per esempio, prima mostri una spia «Carichiamo i regali…», poi un elenco di 10 schede, poi l’utente apre/chiude i filtri. Affinché ChatGPT riservi correttamente lo spazio al widget senza ritagliarlo, quando l’altezza cambia devi comunicare al host il nuovo valore. A questo serve notifyIntrinsicHeight().

Nel template spesso è incapsulato in un hook tipo useAutoResize. Si può implementare così:

import { useEffect, useRef } from 'react';
import { useNotifyIntrinsicHeight } from '@/apps-sdk';

export function useAutoResize() {
  const ref = useRef<HTMLDivElement | null>(null);
  const { notifyIntrinsicHeight } = useNotifyIntrinsicHeight();

  useEffect(() => {
    if (!ref.current) return;

    const observer = new ResizeObserver(entries => {
      for (const entry of entries) {
        notifyIntrinsicHeight(entry.contentRect.height);
      }
    });

    observer.observe(ref.current);
    return () => observer.disconnect();
  }, [notifyIntrinsicHeight]);

  return ref;
}

Esempio d’uso:

export function GiftListInline() {
  const containerRef = useAutoResize();

  return (
    <div ref={containerRef}>
      {/* il tuo contenuto */}
    </div>
  );
}

L’idea è semplice: quando il tuo div radice cambia altezza, chiami l’API dell’SDK e ChatGPT adatta il contenitore. Questo pattern — una “wrapper auto-resizer” attorno a tutto il contenuto — è vivamente consigliato da sviluppatori esperti.

Piccolo schema

Rappresentiamolo come un flowchart:

flowchart TD
    A[Il contenuto del widget è cambiato] --> B[ResizeObserver rileva la nuova altezza]
    B --> C["Chiamata notifyIntrinsicHeight(newHeight)"]
    C --> D[ChatGPT aumenta/riduce il container]
    D --> E[L'utente vede uno scroll pulito senza tagli]

Con dimensioni e altezza abbiamo finito: il widget non deve uscire dallo spazio assegnato né costringere l’utente a un doppio scroll.

5. Tema (theme), colori e bordi: come rendere il widget “nativo”

Se displayMode e maxHeight determinano quanto spazio abbiamo, il tema (theme) e la palette definiscono come quella porzione di interfaccia appare nella chat.

ChatGPT supporta almeno tema chiaro e scuro. La piattaforma lo passa al widget tramite window.openai.theme e/o in _meta["openai/theme"], e nel template React c’è l’hook useOpenAiGlobal("theme") o qualcosa come useTheme.

L’idea principale: la tua UI deve adattarsi al tema, non imporne uno proprio.

Ottenere il tema

Esempio di hook semplice:

import { useOpenAiGlobal } from '@/apps-sdk';

export function useThemeMode() {
  const theme = useOpenAiGlobal<'light' | 'dark'>('theme') ?? 'light';
  return { theme };
}

Nel componente:

export function ThemedCard(props: { children: React.ReactNode }) {
  const { theme } = useThemeMode();

  const className =
    theme === 'dark'
      ? 'bg-slate-900 text-slate-100 border-slate-700'
      : 'bg-white text-slate-900 border-slate-200';

  return (
    <div className={`rounded-xl border p-4 ${className}`}>
      {props.children}
    </div>
  );
}

In un progetto reale probabilmente userai Tailwind con darkMode: 'class' e applicherai la classe dark al contenitore radice del widget. Ma la sostanza non cambia: il tema arriva dall’Apps SDK, non vive per conto suo.

Colori, bordi e tipografia

Secondo le linee guida OpenAI:

  • usa font di sistema e una tipografia equilibrata;
  • non sovrascrivere aggressivamente i colori di sistema;
  • il widget deve essere un elemento “nativo” della chat, non una landing indipendente con gradienti acidi.

Un buon pattern per il container di GiftGenius:

export function GiftCard(props: { title: string; price: string }) {
  return (
    <div className="rounded-xl border border-border bg-background p-3 flex flex-col gap-2">
      <div className="font-medium text-foreground">{props.title}</div>
      <div className="text-sm text-muted-foreground">{props.price}</div>
      <button className="self-start px-3 py-1 text-sm rounded-full bg-primary text-primary-foreground">
        Scegli
      </button>
    </div>
  );
}

Si presume che bg-background, border-border, text-foreground, bg-primary ecc. siano variabili CSS/classi utility collegate al tema di ChatGPT. Questo approccio è descritto nelle raccomandazioni: usare variabili e classi collegate al tema, invece di fissare i colori in modo rigido.

6. Layout e adattività: desktop, mobile, PiP

Il terzo asse è la larghezza e il dispositivo. Semplificando, l’aspetto del widget è determinato dalla modalità (displayMode), dall’altezza disponibile (maxHeight) e dalla larghezza disponibile (desktop/mobile/PiP).

In questa sezione affrontiamo il terzo parametro. Su desktop il widget inline ha una certa larghezza, su mobile un’altra; in PiP lo spazio è davvero poco. L’Apps SDK invia segnali come userAgent, safeArea, talvolta la dimensione del contenitore, leggibili tramite useOpenAiGlobal.

Principi generali

Alcuni principi importanti.

Primo, non fare affidamento su una larghezza fissa. Lo schermo dell’utente può essere stretto (telefono) o largo (grande desktop). Meglio costruire il layout con flex/grid e auto-fit, piuttosto che con un width: 400px rigido.

Secondo, evita lo scroll orizzontale. Se la tua tabella o le schede non ci stanno, meglio passare al fullscreen o mostrare una versione ridotta. Si può anche usare una carousel con slide.

Terzo, considera che PiP/modali sono spesso molto stretti, e non ci si può mettere un form grande — sarebbe scomodo compilare i campi.

Questi punti sono sottolineati nella documentazione: adattività, safeArea, differenza desktop vs mobile e i rischi di layout sovraccarichi.

Layout diversi per inline e fullscreen

Torniamo a GiftGenius. L’elenco dei regali in inline e in fullscreen può differire molto. Creiamo due componenti.

Inline compatto: massimo 3 schede, una colonna su mobile e due su schermo ampio.

export function GiftListInline() {
  const gifts = useGiftData(); // hook fittizio, prendiamo da toolOutput

  return (
    <WidgetContainer>
      <h2 className="text-base font-semibold mb-3">
        Selezione di regali
      </h2>

      <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
        {gifts.slice(0, 3).map(gift => (
          <GiftCard
            key={gift.id}
            title={gift.title}
            price={`${gift.price} $`}
          />
        ))}
      </div>

      {gifts.length > 3 && (
        <p className="mt-3 text-xs text-muted-foreground">
          Sono mostrati i primi 3 risultati. Espandi il widget per vedere tutto.
        </p>
      )}
    </WidgetContainer>
  );
}

E la versione fullscreen: griglia, filtri, più schede.

export function GiftListFullscreen() {
  const gifts = useGiftData();
  const [query, setQuery] = useState('');

  const filtered = gifts.filter(g =>
    g.title.toLowerCase().includes(query.toLowerCase()),
  );

  return (
    <div className="h-full flex flex-col gap-4 p-4">
      <header className="flex gap-2 items-center">
        <h1 className="text-lg font-semibold flex-1">
          Regali per te
        </h1>
        <input
          value={query}
          onChange={e => setQuery(e.target.value)}
          placeholder="Filtro per nome"
          className="px-2 py-1 text-sm border rounded-md flex-1"
        />
      </header>

      <main className="flex-1 overflow-y-auto">
        <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
          {filtered.map(gift => (
            <GiftCard
              key={gift.id}
              title={gift.title}
              price={`${gift.price} $`}
            />
          ))}
        </div>
      </main>
    </div>
  );
}

Qui permettiamo lo scroll verticale interno del contenuto in fullscreen (overflow-y-auto su main), cosa normale in modalità fullscreen. La versione inline, come raccomandano le guide, rimane compatta e “leggibile in 2 secondi”.

Schema: comportamento per modalità

Per fissare il concetto, una semplice diagramma:

stateDiagram-v2
    [*] --> Inline
    Inline: 3 schede, testo minimo
    Inline --> Fullscreen: Clic "Espandi" / "Mostra tutto"
    Fullscreen: Griglia, filtri, molti dati
    Fullscreen --> Inline: Pulsante "Chiudi" / azione dell'host
    Fullscreen --> PiP: Operazione lunga, mostrare progresso
    PiP: Piccolo pannello di stato
    PiP --> Inline: Operazione completata, mostriamo il messaggio finale

Questo scenario è in linea con i pattern UX descritti: inline come teaser, fullscreen come strumento di lavoro, PiP come indicatore di processo.

7. Pratica: due modalità dello stesso widget

Mettiamo in pratica. Come esercizio in questa lezione conviene fare due passi nell’app didattica attuale.

Passo 1. Widget inline con scheda

Estendi l’attuale GiftGenius in modo che in modalità inline il widget:

  • mostri il titolo «Selezione di regali»;
  • visualizzi fino a tre schede regalo da toolOutput;
  • mostri il suggerimento «Espandi il widget per vedere tutto» se i regali sono più di tre;
  • adatti l’altezza in modo pulito tramite useAutoResize e notifyIntrinsicHeight().

Gli stili devono basarsi sul tema: usa classi o variabili collegate al theme, non colori fissi.

Passo 2. Versione fullscreen con form

Poi aggiungi la vista fullscreen che:

  • mostra un titolo + ricerca per nome;
  • mostra tutti i regali in una griglia;
  • permette lo scroll verticale all’interno dell’area principale;
  • fornisce un pulsante «Torna alla conversazione» (che chiama requestDisplayMode('inline')).

La composizione può essere così:

export function GiftGeniusWidget() {
  const { displayMode } = useDisplayMode();

  return (
    <>
      <DisplayModeDebug />
      {displayMode === 'fullscreen' ? (
        <GiftListFullscreen />
      ) : (
        <GiftListInline />
      )}
    </>
  );
}

In ChatGPT Dev Mode potrai cambiare manualmente la modalità o richiedere il fullscreen via codice cliccando «Mostra tutto» nella versione inline (tramite useRequestDisplayMode). Questo esercizio consolida la comprensione di come la stessa App possa apparire e comportarsi in modo diverso a seconda del displayMode.

8. Errori tipici nella gestione dell’aspetto del widget

Prima di proseguire nel corso, fissiamo alcuni errori comuni legati a displayMode, dimensioni, tema e layout. Evitarli da subito rende la vita con l’Apps SDK molto più semplice.

Errore №1: ignorare displayMode e provare a rendere tutto “simile al fullscreen”.
A volte gli sviluppatori disegnano un layout pesante (quasi come uno SPA separato) che entra a fatica in inline. Il risultato è che l’utente vede un Notion in miniatura con scroll e una marea di elementi. L’approccio corretto è progettare viste diverse per modalità diverse e rispettare che inline è un formato compatto, “da un’unica schermata”.

Errore №2: altezza fissa enorme e doppio scroll.
Impostare height: 800px e dimenticarsi di maxHeight porta a un widget o tagliato o con scroll interno ed esterno contemporaneamente. L’utente inizierà a “catturare” la barra giusta, rovinando l’UX. Invece, leggi maxHeight, limita con max-height e, quando l’altezza cambia, comunicalo tramite notifyIntrinsicHeight().

Errore №3: ignorare il tema e “riverniciare tutto col brand”.
Se imposti font, sfondi, gradienti molto contrastati e ignori completamente il tema chiaro/scuro di ChatGPT, rompi l’unità visiva della piattaforma. Le linee guida dicono chiaramente: usa colori e font di sistema e porta il brand con accenti discreti (pulsante, icona, logo). Segui il theme tramite un hook e adatta la palette.

Errore №4: UI troppo complessa in PiP/modali.
Cercare di infilare un form con molti campi in una finestra PiP piccola non funziona. Lì sono appropriati solo casi molto semplici: progresso di un processo, uno-due pulsanti, un singolo campo. Tutto il resto è da fullscreen.

Errore №5: layout rigido a 800px e mancati test su mobile.
Fissare tutto a 800px e pensare che “in qualche modo su telefono entrerà”. In realtà il client mobile di ChatGPT ha un’altra larghezza e un comportamento diverso, e il PiP è ancora più stretto. Non dimenticare userAgent/safeArea, usa grid/flex senza larghezze rigide e testa almeno una volta il tuo widget con un layout stretto.

Errore №6: lavorare direttamente con window.openai senza hook.
Formalmente puoi scrivere const mode = window.openai.displayMode, ma poi dovrai iscriverti agli eventi, occuparti degli aggiornamenti di React e inseguire bug se l’SDK cambia qualcosa. Gli hook (useDisplayMode, useMaxHeight, useOpenAiGlobal, useRequestDisplayMode) esistono proprio per nascondere questa routine e mantenere il codice più pulito. Meglio usarli e vivere sereni.

Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION