2. ¿Para qué sirve el fullscreen si ya existe el inline?
En la lección anterior sobre inline ya acordamos lo siguiente: si la tarea es corta y cabe en 5–7 elementos o en una sola pantalla, la tarjeta inline es la opción ideal. Una lista de varios regalos, un par de filtros, uno o dos botones: todo eso vive perfectamente en el flujo de mensajes.
Pero a cualquier aplicación le llega el momento en que «otra tarjeta más» ya no salva la situación:
- hay que recopilar muchos parámetros (perfil del destinatario, restricciones de entrega, métodos de pago);
- se necesita un asistente de varios pasos;
- hay tablas grandes, gráficos, mapas y descripciones largas.
Inline aquí empieza a sufrir: el ancho está limitado por la columna del chat, la altura también, no hay navegación y, además, el chat tiene un único scroll. Precisamente para estos escenarios el Apps SDK ofrece el modo fullscreen, una interfaz «inmersiva» en la que tu widget ocupa gran parte de la pantalla y puede mostrar un layout complejo.
El segundo protagonista de hoy es PiP, una ventana flotante pequeña que vive por encima del chat. Sus funciones típicas: estado de una tarea en segundo plano, mini‑reproductor, temporizador, indicador de progreso. PiP es ideal cuando algo de larga duración se ejecuta «de fondo» y el usuario sigue conversando con GPT.
Importante: tanto fullscreen como PiP no sustituyen al inline, son una capa adicional. Empezamos con inline y pasamos a fullscreen cuando inline se queda corto; vamos a PiP cuando lo interesante ya está en marcha y solo hace falta «tener a la vista» el estado.
3. Fundamentos técnicos: displayMode y los cambios de modo
Desde el punto de vista de Apps SDK, tu widget tiene un estado de visualización actual: displayMode. En el momento de escribir este curso hay tres modos principales: "inline", "fullscreen" y "pip" (picture-in-picture).
El host (ChatGPT) comunica a tu widget el modo actual mediante datos globales en window.openai y hooks especiales del SDK. En una plantilla típica de React hay algo como:
// alias del template de Apps SDK
const mode = useDisplayMode(); // 'inline' | 'fullscreen' | 'pip'
if (mode === "fullscreen") {
// renderizamos nuestro asistente
} else {
// renderizamos el UI inline compacto
}
El SDK también proporciona el método window.openai.requestDisplayMode({ mode }) y/o el hook useRequestDisplayMode para pedir al host que cambie el modo. Este método devuelve una promesa con el modo realmente establecido, porque la plataforma puede rechazar o corregir tu solicitud (por ejemplo, PiP en móviles casi siempre se convierte en fullscreen).
Esquemáticamente, el ciclo de vida de los modos puede representarse así:
stateDiagram-v2
[*] --> Inline
Inline --> Fullscreen: requestDisplayMode('fullscreen')
Fullscreen --> Inline: requestDisplayMode('inline') / botón "Atrás"
Fullscreen --> PiP: requestDisplayMode('pip')
PiP --> Fullscreen: "Expandir"
PiP --> Inline: finalización de la tarea
Los nombres reales y el conjunto exacto de modos pueden cambiar con las versiones del SDK, por lo que en producción conviene revisar siempre la documentación y no fiarse de «como estaba en el curso».
4. Primer cambio: creamos el botón «Expandir a pantalla completa»
Empecemos con algo pequeño: tomemos nuestro widget inline ya existente, GiftGenius —una App didáctica de módulos anteriores que ahora muestra 3–5 tarjetas de regalos— y añadamos un botón «Abrir selección detallada» para pasar a fullscreen.
Supongamos que tenemos en la plantilla dos 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" });
}}
/>
);
};
Aquí InlineGiftPreview es nuestro UI inline actual, y GiftFullscreenWizard es el nuevo componente asistente que vamos a diseñar ahora. En el manejador onExpand no solo llamamos a requestDisplayMode, sino que esperamos la promesa; así podremos reaccionar más tarde a un rechazo (por ejemplo, mostrar un mensaje si por alguna razón fullscreen no está disponible).
El propio InlineGiftPreview es bastante simple:
type InlineGiftPreviewProps = {
onExpand: () => void;
};
const InlineGiftPreview: React.FC<InlineGiftPreviewProps> = ({ onExpand }) => {
return (
<div>
<h3>Selección de regalos</h3>
{/* ...tarjetas de regalos... */}
<button onClick={onExpand}>Abrir selección detallada</button>
</div>
);
};
De momento se parece mucho a «abrir un modal», pero la diferencia es que no lo controla tu React, sino la aplicación host de ChatGPT, y esta puede mostrar el título, botones del sistema como «Atrás», etc.
5. Diseñamos el asistente en fullscreen de GiftGenius
Ahora diseñemos el asistente en fullscreen para la selección de regalos. Desde el punto de vista de UX, tiene sentido dividir el proceso en varios pasos lógicos. Por ejemplo:
- Quién es el destinatario y cuál es la ocasión.
- Presupuesto y tipo de regalos (físicos, experiencias, digitales).
- Revisión y confirmación de la selección.
En el código esto puede reflejarse con una máquina de estados sencilla por pasos:
type WizardStep = "recipient" | "preferences" | "review";
type WizardState = {
step: WizardStep;
recipient?: { ageRange: string; relation: string };
preferences?: { budget: number; categories: string[] };
};
Creemos el componente GiftFullscreenWizard, que guarda este estado en React y renderiza la pantalla adecuada.
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} />;
};
Cada paso es un pequeño componente con un formulario. Por ejemplo, el primer paso:
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>¿Para quién elegimos el regalo?</h2>
<input
placeholder="¿Quién es para ti?"
value={relation}
onChange={(e) => setRelation(e.target.value)}
/>
<input
placeholder="Edad (p. ej., 25–34)"
value={ageRange}
onChange={(e) => setAgeRange(e.target.value)}
/>
<button
onClick={() =>
onNext({
recipient: { relation, ageRange },
step: "preferences",
})
}
>
Siguiente
</button>
</div>
);
};
En el segundo paso recopilamos presupuesto y categorías; en el tercero llamamos a callTool / a una herramienta MCP que ya sabe elegir regalos con estos parámetros, y mostramos los resultados.
Lo importante es que en la pantalla fullscreen tenemos espacio para:
- una barra de progreso o un stepper;
- campos y ayudas más desarrollados;
- estados de error («algo ha salido mal, inténtalo de nuevo»).
Recomendación de los UX guidelines: cada paso debe seguir siendo lo más simple posible, sin sobrecargar de campos; mejor 3–4 pasos claros que un formulario monstruoso.
6. UX del asistente en fullscreen: progreso, errores, regreso
Simplemente poner el formulario a pantalla completa es la mitad del trabajo. El usuario necesita:
- entender en qué paso se encuentra;
- poder volver atrás;
- ver qué ocurre durante operaciones largas.
Un stepper sencillo puede implementarse de forma puramente visual:
const Stepper: React.FC<{ step: WizardStep }> = ({ step }) => {
const index = step === "recipient" ? 1 : step === "preferences" ? 2 : 3;
return <p>Paso {index} de 3</p>;
};
Y simplemente insertar Stepper en cada pantalla. Una opción más avanzada es renderizar una «escalera» horizontal de pasos, pero en el marco del curso no vamos a montar una escuela de maquetación.
Un punto importante es el manejo de errores. Supongamos que en el último paso llamamos a la herramienta 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,
});
// Los resultados aparecerán después en el chat / el widget
} catch (e) {
setError("No se pudieron seleccionar regalos, inténtalo de nuevo.");
} finally {
setLoading(false);
}
};
return (
<div>
{/* mostrar el resumen de parámetros */}
{error && <p style={{ color: "red" }}>{error}</p>}
<button disabled={loading} onClick={handleConfirm}>
{loading ? "Buscando…" : "Confirmar y buscar"}
</button>
</div>
);
};
Desde el punto de vista de accesibilidad, hay que cuidar que:
- en fullscreen los botones «Siguiente», «Atrás» y «Cancelar» sean fácilmente clicables;
- el texto tenga un contraste adecuado;
- sea posible recorrer con Tab todos los elementos interactivos en orden.
Si puedes, merece la pena añadir aria-label a controles no estándar (por ejemplo, conmutadores personalizados de categorías). Aunque este curso no es un examen de WCAG, una atención básica a la a11y te ayudará a pasar la revisión del Store sin dolores innecesarios.
En definitiva, el asistente fullscreen resuelve escenarios complejos y de varios pasos: ofrece espacio para formularios, progreso y errores. Pero la vida de la aplicación no termina ahí: muchas tareas continúan «en segundo plano». Para eso tenemos el segundo modo: PiP, del que hablaremos a continuación.
7. Qué es PiP en el mundo de ChatGPT y por qué es «caprichoso»
Ya vimos cómo usar fullscreen para escenarios complejos. Ahora veamos el caso contrario: cuando lo importante ya está en marcha y solo hay que «controlar» el progreso. Aquí entra en juego PiP.
En la web, «picture-in-picture» suele asociarse con vídeo que queda en una esquina por encima del contenido. En ChatGPT, PiP es una pequeña ventana flotante del widget, que permanece visible al hacer scroll en el chat y puede mostrar estado, progreso o un UI compacto.
Varias particularidades importantes que conviene conocer por documentación y por la experiencia de early adopters:
- PiP tiene muy poco espacio. No es un lugar para formularios ni layouts complejos, sino para dos o tres métricas clave y uno o dos botones.
- En escritorio, PiP «se fija» arriba y queda visible con cualquier desplazamiento; en móviles a menudo se convierte automáticamente en fullscreen.
- La solicitud requestDisplayMode con mode "pip" no garantiza un PiP real. La plataforma puede devolver otro modo (por ejemplo, fullscreen) o incluso comportarse de forma extraña en versiones antiguas del SDK, así que revisa siempre el resultado de la promesa y ten un fallback.
De aquí se desprende una conclusión simple de UX: en PiP solo lo más importante. Temporizador, indicador de entrega, estado de la tarea, botón «Expandir». Nada de 12 casillas, tablas de 10 columnas ni «hazme otro café».
8. GiftGenius + PiP: búsqueda larga y progreso en segundo plano
Volvamos a GiftGenius. Imaginemos este escenario: el usuario ha pasado por el asistente fullscreen, ha pulsado «Confirmar», y ahora tu backend lanza una selección bastante pesada —quizá mediante un servidor MCP llamas a varias APIs externas, recalculas precios, aplicas muchos filtros. Esto puede tardar, digamos, 10–20 segundos.
Desde el punto de vista de UX no queremos retener 20 segundos al usuario en fullscreen con un spinner girando. Mejor:
- Iniciar la selección.
- Contraer la interfaz a PiP, mostrando el progreso.
- Permitir que el usuario siga chateando (por ejemplo, hacer preguntas aclaratorias).
- Al terminar, devolver el resultado en inline o abrir un nuevo fullscreen con los regalos.
Hagamos un hook sencillo que gestione este comportamiento:
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("Modo efectivo:", resultMode.mode);
await callTool("run_gift_job", payload);
setStatus("done");
await requestDisplayMode({ mode: "inline" });
};
return { status, startJob };
};
Ahora, en ReviewStep en lugar de llamar directamente a callTool usamos este hook:
const ReviewStep: React.FC<StepProps> = ({ state }) => {
const { status, startJob } = useLongGiftJob();
return (
<div>
{/* ...resumen... */}
<button
disabled={status === "running"}
onClick={() => startJob(state)}
>
{status === "running" ? "Buscando regalos…" : "Iniciar selección"}
</button>
</div>
);
};
Para que el estado de la tarea en segundo plano esté disponible tanto para el asistente fullscreen como para la ventana PiP, en código real conviene extraer useLongGiftJob a un contexto y leerlo mediante useLongGiftJobContext. Omitimos los detalles de implementación del contexto (Provider, createContext): lo importante es que el estado del job viva en un único lugar y las distintas capas de UI simplemente se suscriban a él.
Y un componente separado para la vista PiP:
const GiftPipView: React.FC<{ status: string }> = ({ status }) => {
return (
<div>
<p>GiftGenius está trabajando…</p>
<p>Estado: {status === "running" ? "en curso" : "listo"}</p>
<button
onClick={() => window.openai.requestDisplayMode({ mode: "fullscreen" })}
>
Expandir
</button>
</div>
);
};
En el widget general sustituimos el render para que tenga en cuenta también PiP:
const GiftGeniusWidget: React.FC = () => {
const mode = useDisplayMode();
const { status } = useLongGiftJobContext(); // a través del contexto, como comentamos arriba
if (mode === "pip") {
return <GiftPipView status={status} />;
}
if (mode === "fullscreen") {
return <GiftFullscreenWizard />;
}
return <InlineGiftPreview onExpand={/* como antes */} />;
};
Este escenario combina perfectamente con los modos de voz (hablaremos de ello en la lección sobre voz): con la voz lanzamos la selección, PiP muestra el progreso y el chat queda abajo y sigue con su vida.
9. Vídeo + chat: cuando fullscreen y PiP se convierten en reproductor multimedia
Históricamente, PiP se asocia sobre todo con vídeo que queda en la esquina por encima del contenido. Por eso tiene sentido analizar aparte el escenario «vídeo + chat». Tampoco hay magia aquí: en la mayoría de los casos solo muestras el vídeo en fullscreen o en la ventana PiP. La documentación de OpenAI pone los escenarios de medios como ejemplo típico del uso de fullscreen y PiP.
¿Qué puede significar esto para GiftGenius? Por ejemplo:
- muestras un vídeo promocional del regalo;
- un tutorial corto «cómo envolver un regalo con estilo»;
- una reseña en vídeo de varios productos.
En fullscreen podemos renderizar un <video> completo con descripción y recomendaciones; en PiP dejar solo el reproductor y, quizá, un pequeño título.
Un componente contenedor sencillo:
const GiftVideoPlayer: React.FC<{ src: string; title: string }> = ({
src,
title,
}) => (
<div>
<h3>{title}</h3>
<video
src={src}
controls
style={{ width: "100%", borderRadius: 8 }}
/>
</div>
);
En el asistente fullscreen podemos ofrecer al usuario «Ver la reseña en vídeo de este regalo», y luego minimizarlo a PiP:
const WatchVideoStep: React.FC = () => {
const requestDisplayMode = useRequestDisplayMode();
return (
<div>
<GiftVideoPlayer src="/videos/gift-wrap.mp4" title="Cómo envolver un regalo" />
<button
onClick={() => requestDisplayMode({ mode: "pip" })}
>
Dejar el vídeo en la esquina y volver al chat
</button>
</div>
);
};
Un par de consejos prácticos para escenarios multimedia:
- no actives el autoplay con sonido: es un antipatrón de UX universal;
- cuida los subtítulos y la posibilidad de pausar con el teclado (espacio, flechas);
- en la ventana PiP no intentes mostrar todo el texto auxiliar, limítate al propio vídeo.
10. Estado, recreación del widget y particularidades móviles
La pregunta más desagradable que suele surgir en este punto: «¿Se conservará el estado de React si cambio de inline a fullscreen y regreso?»
Respuesta corta: no confíes en ello.
Técnicamente el comportamiento depende de la versión del SDK y de la implementación del host: en unos casos la transición entre modos ocurre sin recrear el iframe; en otros, el widget se desmonta y se vuelve a montar. La documentación subraya que la conservación del contexto al cambiar de modo depende de la implementación concreta del SDK y su versión, y no es una garantía para el desarrollador.
Enfoque práctico:
- Guarda todo el estado crítico (paso del asistente, datos introducidos, identificador de la tarea en segundo plano) bien:
- en el backend (a través de tu servidor MCP y tokens de sesión),
- o en el contexto de ChatGPT (por ejemplo, mediante tools que devuelvan «el estado actual del workflow»),
- o en parámetros de URL/almacenamiento local, si hay una base segura para ello.
- Usa el estado de React como caché/capa de UI, pero prepárate para que al cambiar de modo pueda resetearse: entonces lo restauras desde la fuente más fiable.
La segunda sutileza tiene que ver con el resultado de requestDisplayMode. Como ya se mencionó, una solicitud con mode "pip" puede volver como "fullscreen", especialmente en móviles, donde el PiP real puede no estar soportado o convertirse automáticamente en pantalla completa.
Patrón típico:
const requestDisplayMode = useRequestDisplayMode();
const openPipSafe = async () => {
const result = await requestDisplayMode({ mode: "pip" });
if (result.mode !== "pip") {
// Fallback: por ejemplo, mostrar un mensaje o adaptar el UI a fullscreen
console.log("PiP no disponible, trabajamos en modo:", result.mode);
}
};
Así no acabarás en la situación de haber planificado una ventanita y recibir un UI a pantalla completa con botones «específicos de PiP». En ese modo, esa interfaz se verá rara.
Por último, recuerda maxHeight y el scroll interno: incluso en fullscreen el host puede limitar la altura del contenedor, y tu misión es organizar el scroll de modo que no aparezcan tres barras de desplazamiento anidadas.
11. Errores habituales al trabajar con fullscreen y PiP
Error n.º 1: fullscreen como modo predeterminado.
Algunos desarrolladores ven la palabra «fullscreen» e intentan convertir su App en un SPA independiente dentro del chat. Resultado: cualquier mención de regalos y el usuario vuela al asistente a pantalla completa, aunque solo quería un par de ideas. Los guidelines de OpenAI recomiendan insistentemente empezar con inline y ampliarse a fullscreen solo cuando haya una necesidad objetiva.
Error n.º 2: PiP como un fullscreen en miniatura.
PiP tiene un área muy limitada, pero a veces se intenta meter allí todo: pestañas, formularios, filtros. El usuario recibe una interfaz microscópica imposible de manejar con el ratón. El enfoque correcto es mostrar en PiP solo el estado y uno o dos botones clave (por ejemplo, «Expandir» y «Cancelar»).
Error n.º 3: transiciones inexplicadas entre modos.
Cuando el widget se abre de repente en fullscreen sin texto de GPT o sin un clic explícito del usuario, desorienta. Lo mismo vale para el auto-minimizado a PiP o el regreso a inline. Cada transición debe acompañarse de una breve explicación en el mensaje del modelo: «Ahora abriré el asistente detallado» antes de fullscreen, «Minimizaré la selección a una ventana pequeña mientras se calcula» antes de PiP.
Error n.º 4: ignorar móviles y diferencias entre plataformas.
El desarrollador prueba solo en escritorio, donde PiP se comporta como se espera, y luego en móvil todo se convierte en fullscreen, el layout «se rompe» y los botones quedan fuera del safe area. La documentación advierte que PiP en móvil puede implementarse como fullscreen y que el comportamiento puede cambiar entre versiones del SDK, por lo que probar en los dispositivos objetivo y trabajar con requestDisplayMode con cuidado es obligatorio.
Error n.º 5: fe ciega en la persistencia del estado al cambiar de modo.
Confiar solo en el estado de React sin soporte servidor/persistente alguno lleva a situaciones cómicas: el usuario completó dos pasos del asistente, pulsó «Minimizar a PiP» y, tras volver, acabó en el primer paso con los campos vacíos. Es mejor asumir que al cambiar de modo tu componente puede desmontarse y diseñar la gestión del estado con ese riesgo en mente.
Error n.º 6: olvidar la accesibilidad del asistente en fullscreen.
Un formulario bonito en pantalla grande no siempre es cómodo para personas con baja visión o quienes usan solo teclado. Texto demasiado pequeño, bajo contraste, botones «Siguiente» y «Atrás» ilegibles: causas frecuentes no solo de mal UX, sino también de problemas en la revisión del Store. Conviene comprobar al menos lo básico: contraste del texto, tamaño de fuente, navegación con Tab y la existencia de etiquetas textuales claras para los botones.
GO TO FULL VERSION