Guía de UI personalizada
El código de interfaz es una pequeña app de React (TSX) que dibuja la pantalla que ve el jugador. Puede cambiar el estilo del chat, añadir paneles alrededor o sustituirlo por algo que no se parezca en nada a un chat. Está en Paneles → Código de frontend.
Buena parte de esto ya no necesita código. El editor de la Interfaz del jugador monta pantallas de inicio, paneles de estado, inventarios, mapas y botones a partir de plantillas, y sus botones pueden cambiar variables, activar comportamientos y llamar a IA. Recurre al código de interfaz cuando las plantillas y las partes no hagan lo que quieres. Aun así, la mayoría de los creadores no lo escriben a mano: le describen la pantalla al Asistente IA, que escribe el código.
Para lo básico y la configuración, mira Para empezar: Imagen y sonido.
Tres formas de que se vea una carta
Chat por defecto (sin UI personalizada)
Los mensajes aparecen como burbujas de texto, con un cuadro para escribir abajo, y todo se desplaza con normalidad. Así empieza toda carta nueva, y a muchas no les hace falta más.
Ideal para: rol cotidiano, aventuras sencillas, charlas con personajes, cualquier carta en la que lo que importa es la escritura.
El Aspecto de los mensajes de la Interfaz del jugador (Plataforma, Burbujas, Novela, Carta, Terminal) y sus opciones de ajuste fino cambian el estilo de este chat sin código.
Burbujas de mensaje personalizadas
Conservas el chat completo (desplazamiento, streaming, swipes, cuadro de texto) y cambias cómo se ve cada mensaje.
Lo que se suele hacer con esto:
- Fondos y fuentes a juego con el ambiente de la carta
- Retratos de los personajes junto al diálogo
- Una barra de estadísticas (vida, oro, afinidad) visible en cada mensaje
- Estilo de juego de terror, con tonos oscuros y fuentes inquietantes
- Un color para cada personaje en las escenas con varios
Modo de aplicación completa
Tu código dibuja la pantalla entera. El chat pasa a ser un componente más de tu diseño, o te lo saltas y montas otra cosa.
Lo que se suele hacer con esto:
- Pantallas de novela visual con fondos de escena y sprites de personajes
- Mapas en los que hacer clic en un lugar envía un mensaje
- Pantallas de combate por turnos
- Simuladores de móvil en los que cada «app» provoca un comportamiento distinto de la IA
- Paneles interactivos, pantallas de inventario, diarios de misiones
Battle Royale usa el modo de aplicación completa para su vista táctica. Still lo usa para su interfaz de puzles.
Empieza con el chat por defecto. Si la carta necesita ambiente visual, cambia el aspecto de los mensajes o añade burbujas personalizadas. Si necesita paneles interactivos, una disposición de videojuego o algo que no sea un chat, usa la Interfaz del jugador, y pasa al modo de aplicación completa cuando no te baste.
Montarla con el Asistente IA
Cómo funciona
Haz clic en Asistente IA, arriba a la derecha del lienzo, y describe con palabras normales el aspecto que quieres. Escribe el código de interfaz en Paneles → Código de frontend, y la vista previa de la Interfaz del jugador en el lienzo muestra el resultado.
Buenos prompts
Cuanto más concreta sea la descripción, mejor sale. Describe la disposición, el ambiente y qué variables mostrar.
Vago (la IA tiene que adivinarlo todo):
«Que quede chulo»
Bueno (disposición y ambiente claros):
«Quiero una interfaz oscura, de terror. Barra de vida roja arriba, mensajes con aspecto de texto de máquina de escribir antigua sobre papel amarillento, una niebla oscura alrededor de los bordes.»
Muy bueno (disposición + ambiente + variables concretas + comportamiento):
«Monta una disposición de novela visual. Imagen de fondo a pantalla completa sacada de la variable
scene_bg. Retrato del personaje a la izquierda, decharacter_sprite. Un cuadro de diálogo semitransparente abajo, con el nombre del personaje despeaker_nameen rosa. Cuando la afinidad pase de 75, añade un efecto sutil de partículas de corazones.»
Trabaja por rondas
- Describe la idea general: «Quiero una disposición de novela visual con fondos de escena y retratos de personajes»
- Mira la vista previa: ¿la disposición encaja? ¿El espaciado está bien?
- Ajusta los detalles: «Haz el cuadro de diálogo más transparente. Pasa el retrato del personaje a la derecha. Usa una fuente con serifa para el diálogo.»
- Dale el último toque: «Añade un fundido cuando cambie la escena. Haz que el medidor de afinidad brille cuando suba.»
Varias rondas cortas te llevan antes al resultado que una descripción larga.
Qué contarle al asistente sobre tus variables
El asistente puede leer tus variables, pero ayuda explicar lo que importa:
«Mis variables: health (0-100, como barra roja), gold (número, como texto con un icono de moneda), location (texto como 'forest' o 'cave', arriba a la derecha), is_night (booleano; cuando es true, oscurece el fondo)»
Lo que puede hacer el código de interfaz
Tu código habla con el juego a través de useYumina(). Esto es lo que puedes montar con él:
Leer el estado del juego
Tu UI puede leer cualquier variable, el historial completo de mensajes, quién es el jugador, qué modelo está elegido, si la IA está generando en ese momento y más. Así funcionan las barras de estadísticas, los inventarios y los diarios de misiones: leen variables y las muestran.
api.variables.hunger busca la variable cuyo ID es hunger y, si no la encuentra, la que tiene hunger como nombre visible. Las dos formas funcionan, así que un HUD hecho con los nombres que ves en el lienzo lee los valores reales aunque los ID sean UUID aleatorios. Lo mismo al escribir: api.setVariable("hunger", 50) va a esa variable. Las reglas completas: ID frente a nombre visible.
Controlar el chat
Tu UI puede enviar mensajes como el jugador, editar o borrar mensajes, pedirle a la IA que regenere, cortar la generación a medias o reiniciar la conversación. Un botón «Beber la poción», por ejemplo, envía ese texto como mensaje del jugador y la IA responde.
Activar comportamientos
api.executeActionAndWait("buy", { price: 5 }) ejecuta los comportamientos con el disparador El jugador pulsa un botón cuyo Botón se llama buy, y les pasa valores. El comportamiento los lee como {param.price}. Las tiendas, la fabricación o las tiradas de combate pueden vivir en comportamientos del lienzo, y tu código solo tiene que decir qué objeto se ha pulsado. Mira Comportamientos · Pasar un valor desde un botón.
Llamar a las IA de la carta
Una IA Por interfaz del lienzo espera a que tu interfaz la llame. api.callAi("Fortune Mushroom", input) la ejecuta y devuelve { text, fields }: lo que dice y los valores que ha rellenado en Formato de respuesta. Su modelo, lo que ve, los reintentos, el enfriamiento y las frases de reserva se configuran en el lienzo, y la respuesta se comprueba antes de llegar a tu código. Con Dónde van sus palabras en Un canal de la interfaz, tu código también puede escuchar lo que dice con api.onAiOutput(channel, cb). Mira IA.
Llamadas directas a la IA
Cuando prefieras escribir tú todo el prompt, ai.complete() hace una llamada aparte a la IA que nunca toca el chat principal. El jugador no la ve como un mensaje del chat, y no afecta al historial ni al estado de la conversación principal. Para qué sirve:
- Conversaciones por móvil con PNJ: un personaje tiene su propia ventana de chat dentro de tu UI. El jugador le escribe, la IA contesta con la voz de ese personaje y la historia principal sigue por su lado. Cada personaje secundario puede tener su propio prompt de sistema y su personalidad.
- Descripciones de objetos escritas por la IA: el jugador pasa el cursor por un objeto del inventario y la IA escribe en el momento una descripción única según cómo va la historia.
- Sistemas de pistas: un botón de «pensar» que analiza la situación del jugador y le da un empujoncito sin que la IA principal salga del personaje.
- Paneles de monólogo interior: un panel lateral con lo que piensa un PNJ, generado por un prompt distinto del que lleva el diálogo.
- Paneles de traducción o resumen: resúmenes o traducciones de la conversación escritos por la IA, junto al chat principal.
Pasa includeLorebook: "matched" para que la IA secundaria vea el mismo lore y los mismos perfiles de personajes que el chat principal. Omítelo para tareas que no necesitan el contexto de la carta (traducciones, clasificaciones, utilidades sin más).
Las llamadas secundarias tienen su propio límite de frecuencia (100 por minuto, aparte del chat principal) y se cobran por token igual que el chat principal. La firma completa del método, los límites y las opciones de includeLorebook están en la Referencia API.
Inyección de contexto invisible
Tu UI puede enviar un mensaje que la IA principal ve en su siguiente turno y que el jugador nunca ve en el chat. Llama a injectContext() y el motor mete un mensaje de sistema (o de usuario) de un solo uso en el siguiente prompt, y luego lo descarta solo.
Así consigues que la IA reaccione a cosas que pasan fuera de la conversación principal:
- Sucesos fuera de escena: «Cuando te fuiste, el PNJ murmuró para sí: "No puedo dejar que encuentren la carta"». La IA lo incorpora a su siguiente respuesta.
- Cambios en el entorno: «Ha empezado a llover. La entrada de la cueva está medio inundada». El jugador no ve esta instrucción, pero la IA describe la lluvia.
- Consecuencias de la UI: cuando el jugador pulsa un botón de tu UI personalizada (por ejemplo, robar en una tienda), inyecta contexto que le cuente a la IA lo que ha pasado para que pueda reaccionar.
- Mensajes y avisos del móvil: «Te acaba de llegar un mensaje misterioso: "Esta noche, a las 9, en el sitio de siempre"». La IA lo mete en la narración sin que el jugador vea un mensaje del sistema.
ai.complete() hace una llamada aparte a la IA; injectContext() alimenta la siguiente respuesta de la IA principal. Usa ai.complete() cuando quieras otra voz de IA, e injectContext() cuando quieras que la IA principal sepa algo que el jugador no ha dicho. Para algo que sigue siendo cierto turno tras turno, como en qué sala está el jugador o qué hay en pantalla, usa setScene(): todas las llamadas a la IA lo reciben hasta que lo cambies.
La firma del método está en la Referencia API.
Reproducir audio
Tu UI puede poner música de fondo y efectos de sonido, hacer fundidos entre pistas y controlar el volumen. Junto con las variables, puedes tener música que cambie según el lugar o el ambiente.
Guardar y cargar
Hay dos tipos de almacenamiento. api.storage guarda texto en este navegador, por carta: preferencias, la pestaña que el jugador tenía abierta, un récord en este dispositivo. api.sessionStorage guarda pequeños registros JSON con la partida en el servidor, así que acompañan al jugador a otros dispositivos y a los puntos de control.
Navegar y avisar
Activar o quitar el modo inmersivo, mostrar notificaciones, copiar texto al portapapeles, cambiar de apertura, abrir el gestor de personas o el selector de modelo.
Dónde está la API completa
La referencia completa, método a método, con firmas de tipos y ejemplos, está en dos sitios:
- Referencia API: todos los métodos y campos, con ejemplos
- Especificación del mundo: UI personalizada: la especificación pensada para que la lea una IA
Pásale la Especificación del mundo a tu propia IA (Claude, ChatGPT, Cursor) cuando montes una UI compleja.
Las tres formas de personalizarla
Opción 1: pedírselo al Asistente IA
Describes lo que quieres, el asistente escribe el código y lo vas afinando hablando con él.
Ventajas: no hace falta saber programar. Se itera rápido. El asistente conoce la API y se ocupa de detalles como el streaming, los estados vacíos y la disposición en móvil.
Cuándo: empieza aquí. Cambia a otra opción solo si te topas con algo que el asistente no sabe hacer.
Opción 2: usar tu propia IA (Claude, ChatGPT, Cursor)
Conecta tu IA, en la barra superior, deja que Claude, ChatGPT / Codex, Cursor, Claude Code o Codex CLI inicien sesión y editen tu carta directamente, código de interfaz incluido. Mira El Asistente IA y tu propia IA.
Si prefieres pegar el código tú, cuéntale a la IA:
- Tu código es TSX (React) y se ejecuta en un iframe aislado (sandbox)
- Todo está disponible como global: React, useYumina, Icons, Chat, MessageList, MessageInput, Tailwind CSS
- El archivo de entrada es
index.tsxconexport default function MyWorld() { ... } - El estado del juego sale de
useYumina(): variables, mensajes, estado del streaming, todo - Hay que usar
React.useStatey compañía, no hooks importados
Cuándo: UI complejas de varios archivos, cuando quieres una conversación más larga o cuando ya trabajas en un editor de código con IA.
Opción 3: escribirla a mano (para desarrolladores con experiencia)
Abre Paneles → Código de frontend y escribe TSX directamente. La insignia de compilación muestra OK o Error mientras escribes.
Cuándo: eres desarrollador y piensas en React, o quieres controlar cada detalle.
Normas básicas del código de UI personalizada
Valen sigas la opción que sigas. El Asistente IA ya las cumple; esta sección sirve para entender qué pasa por debajo o para depurar cuando algo falla.
Las cinco normas
1. Formato del archivo de entrada
index.tsx tiene que exportar por defecto un componente función. Es la raíz de tu UI:
export default function MyWorld() {
return <Chat />;
}2. Globales: no los importes
Estos ya están disponibles en todas partes, sin importarlos: React, useYumina, Icons, Chat, MessageList, MessageInput, useAssetFont, los controles de lore (LoreSlot, LoreButton, ...), los componentes del selector de modelo y todas las clases de Tailwind CSS. La lista completa está en la Referencia API.
Las sentencias import se eliminan al compilar el código, así que import React from "react" no hace nada, e import { useState } from "react" deja useState sin definir.
3. Tus propios archivos SÍ se pueden importar
El código de interfaz de varios archivos usa la sintaxis de módulos ES:
import StatBar from "./stat-bar"
import DialogueBox from "./dialogue-box"4. Usa React.useState(), no useState()
React está disponible como módulo, pero los hooks no vienen sueltos. Ponles siempre delante React.:
var [count, setCount] = React.useState(0)5. Los tipos de TypeScript se eliminan, no se comprueban
Las anotaciones de tipo y las interfaces se quitan al compilar, así que no rompen nada, pero tampoco hay nada que las compruebe. El Asistente IA escribe JavaScript normal en JSX. const, let, var y las funciones flecha funcionan.
Patrones comunes
Estas son las piezas que combinan la mayoría de las interfaces personalizadas. Los ejemplos de código se pueden desplegar: están como referencia, y el Asistente IA escribe este tipo de código por ti.
Burbujas de mensaje personalizadas
Conserva el chat completo y cambia cómo se ven los mensajes. Usa <Chat renderBubble={...} /> para encargarte de dibujar las burbujas mientras la plataforma se ocupa del resto (desplazamiento, streaming, swipes, cuadro de texto).
Cuándo: quieres mensajes con estilo propio (terror oscuro, romance elegante, terminal de ciencia ficción) sin rehacer todo el chat, y las opciones de Aspecto de los mensajes de la Interfaz del jugador se quedan cortas.
Ejemplo de código: burbujas con estilo y estadísticas
export default function MyWorld() {
var api = useYumina()
return (
<Chat renderBubble={function(msg) {
if (msg.role === "user") {
return (
<div className="ml-auto max-w-[80%] rounded-xl bg-blue-500/20 px-4 py-3 text-blue-100">
{msg.rawContent}
</div>
)
}
return (
<div className="mr-auto max-w-[85%] rounded-xl border border-zinc-700 bg-zinc-900 p-4">
<div dangerouslySetInnerHTML={{ __html: msg.contentHtml }} />
<div className="mt-3 flex gap-4 text-xs text-zinc-400">
<span>HP {api.variables.health}/100</span>
<span>Gold {api.variables.gold}</span>
</div>
</div>
)
}} />
)
}Estadísticas y HUD
Un panel fijo con la vida, el oro, la afinidad, el lugar o cualquier otra variable. Suele ir encima del chat (con la prop children de <Chat>) o al lado (con flex). Las plantillas Panel de estadísticas y HUD de aventura de la Interfaz del jugador lo montan sin código.
Cuándo: tu carta lleva estadísticas que el jugador tiene que ver todo el rato: juegos de rol, de supervivencia, simuladores de citas con medidor de afinidad.
Ejemplo de código: barra HUD superior
export default function MyWorld() {
var api = useYumina()
return (
<Chat>
<div className="shrink-0 px-4 py-2 bg-black/60 backdrop-blur flex gap-4 text-xs text-zinc-300">
<div className="flex items-center gap-1">
<Icons.Heart className="w-3 h-3 text-red-400" />
<span>{api.variables.health || 100}/100</span>
</div>
<div className="flex items-center gap-1">
<Icons.Coins className="w-3 h-3 text-amber-400" />
<span>{api.variables.gold || 0}</span>
</div>
<div className="ml-auto text-zinc-500">
{api.variables.location || "Unknown"}
</div>
</div>
</Chat>
)
}Disposición de novela visual
Fondo de escena a pantalla completa, sprite del personaje y un cuadro de diálogo semitransparente abajo. Suele leer los datos de la escena y del personaje de variables que la IA va actualizando según avanza la historia. El montaje completo está en Receta: modo novela visual.
Cuándo: romance, drama, historias cotidianas en las que el ambiente visual importa más que una interfaz de chat clásica.
Ejemplo de código: base de novela visual con fondos de escena
export default function MyWorld() {
var api = useYumina()
var bg = api.variables.scene_bg
var sprite = api.variables.character_sprite
var speaker = api.variables.speaker_name
var lastMsg = (api.messages || []).slice(-1)[0]
return (
<div
className="relative w-full h-full bg-cover bg-center"
style={{
backgroundImage: bg
? "url(" + bg + ")"
: "linear-gradient(135deg, #1e293b, #0f172a)"
}}
>
{sprite && (
<img
src={sprite}
className="absolute bottom-0 left-1/2 -translate-x-1/2 max-h-[80%] pointer-events-none"
/>
)}
<div className="absolute inset-x-4 bottom-4">
<div className="rounded-xl border border-white/10 bg-black/70 p-4 backdrop-blur-sm">
{speaker && (
<div className="mb-1 text-sm font-bold text-pink-300">{speaker}</div>
)}
<div className="leading-relaxed text-zinc-100">
{lastMsg ? lastMsg.content : ""}
</div>
</div>
<div className="mt-2">
<MessageInput />
</div>
</div>
</div>
)
}Panel de juego lateral
El chat a la izquierda y un panel fijo a la derecha con la información del personaje, las estadísticas, el inventario o un mapa. El jugador conserva el chat completo y puede consultar la información del juego mientras habla.
Cuándo: juegos de rol, de aventuras, cualquier carta en la que el jugador necesite mirar estadísticas o inventario mientras chatea.
Ejemplo de código: chat + barra lateral
export default function MyWorld() {
var api = useYumina()
return (
<div className="flex h-full">
<div className="flex-1 min-w-0">
<Chat />
</div>
<aside className="w-72 shrink-0 border-l border-border bg-card p-4 overflow-y-auto">
<div className="text-sm font-bold mb-3">{api.variables.player_name || "Adventurer"}</div>
<div className="space-y-2 text-xs">
<div className="flex justify-between">
<span className="text-muted-foreground">HP</span>
<span>{api.variables.health || 100}/{api.variables.max_health || 100}</span>
</div>
<div className="h-1.5 rounded-full bg-zinc-800 overflow-hidden">
<div
className="h-full bg-red-500 transition-all duration-300"
style={{ width: ((api.variables.health || 100) / (api.variables.max_health || 100) * 100) + "%" }}
/>
</div>
</div>
<div className="mt-4 text-xs text-muted-foreground">
<div className="font-medium mb-2">Inventory</div>
<div className="grid grid-cols-3 gap-1">
{(api.variables.inventory || []).map(function(item, i) {
return (
<div key={i} className="aspect-square rounded border border-border bg-muted flex items-center justify-center text-[10px]">
{item.name || "?"}
</div>
)
})}
</div>
</div>
</aside>
</div>
)
}Botones y opciones interactivas
Botones que envían mensajes, cambian variables o activan comportamientos al pulsarlos. Un uso habitual es la apertura: convertirla en una pantalla de creación de personaje, un selector de dificultad o un comienzo de historia con ramas.
La Interfaz del jugador hace casi todo esto sin código: botones con los pasos Decir una frase por el jugador, Cambiar una variable, Activar un comportamiento y Llamar a una IA, y plantillas de inicio como Elegir inicio, Escribir tu nombre y Elegir dificultad. Usa código cuando los botones tengan que estar dentro de una burbuja del chat o dependan de una lógica que los pasos no pueden expresar.
Cuándo: cualquier carta en la que quieras que el jugador elija entre opciones en lugar de escribir (o además de escribir).
Ejemplo de código: la apertura como creación de personaje
export default function MyWorld() {
var api = useYumina()
return (
<Chat renderBubble={function(msg) {
if (msg.messageIndex === 0 && msg.role === "assistant") {
return (
<div className="space-y-4">
<div dangerouslySetInnerHTML={{ __html: msg.contentHtml }} />
<div className="flex gap-3">
<button
onClick={function() {
api.setVariable("class", "Warrior")
api.sendMessage("I choose Warrior")
}}
className="px-4 py-3 rounded-lg border border-zinc-600 hover:bg-zinc-800 transition"
>
Warrior
</button>
<button
onClick={function() {
api.setVariable("class", "Mage")
api.sendMessage("I choose Mage")
}}
className="px-4 py-3 rounded-lg border border-zinc-600 hover:bg-zinc-800 transition"
>
Mage
</button>
</div>
</div>
)
}
return <div dangerouslySetInnerHTML={{ __html: msg.contentHtml }} />
}} />
)
}Errores comunes
Añadir UI personalizada sin necesitarla. El chat por defecto es limpio, rápido y lo mantiene la plataforma. Si lo fuerte de tu carta es la escritura, el chat por defecto mantiene al jugador centrado en ella.
Olvidarse del streaming. Mientras la IA genera, msg.isStreaming es true y el contenido está incompleto. Tu burbuja tiene que llevar bien el texto a medias: no analices el contenido dando por hecho que está completo.
No probar en móvil. Muchos jugadores usan el móvil. Si tu barra lateral mide 320 px de ancho, no cabe en la pantalla de un móvil. Usa clases responsive de Tailwind (hidden md:block para ocultar paneles en pantallas pequeñas) o prueba la disposición con anchos estrechos.
Bloquear la entrada de texto. Si vas al modo de aplicación completa y te olvidas de poner <MessageInput /> (o tu propio cuadro que llame a api.sendMessage()), el jugador no puede hablar con la IA. Asegúrate siempre de que hay forma de enviar mensajes.
Cambiar variables antes de cambiar de apertura. api.switchGreeting() devuelve las variables a los valores iniciales de esa apertura. Espera a que termine (await) y luego pon tus variables.
Importar los globales. Las sentencias import se eliminan, así que import { useState } from "react" deja useState sin definir. React, useYumina, Icons, Chat, MessageList, MessageInput: todos son globales. No los importes.
Consulta también
- Referencia API: la referencia completa, método a método, de
useYumina(),callAi(),ai.complete(),injectContext()y todos los demás métodos del puente - Interfaz del jugador: plantillas y partes, sin código
- Diseñando el estado del juego: las variables que tu UI lee y muestra
- 3D, panoramas y recursos: qué puede cargar el sandbox y cómo saltar entre lugares 3D
- Diseño de audio: reproducir audio desde tu UI personalizada con la API del puente
- Directivas y macros de IA: cómo actualiza la IA el estado que dibuja tu UI
Especificación para máquinas → Especificación del mundo: UI personalizada
