Referencia API
La lista completa de todo lo que el sandbox expone: globales, componentes, cada campo y método en
useYumina(), definiciones de tipos y los reemplazos para las APIs de navegador bloqueadas.
Esta es documentación de referencia, no un tutorial. Lee primero la Guía de UI personalizada para obtener la imagen general; ven aquí para buscar firmas específicas.
Todo en esta página se deriva de la implementación real en packages/app/sandbox/, por lo que coincide con la versión del sandbox enviada con el editor.
Globales del sandbox
Estos nombres están disponibles en todas partes de tu árbol de root component sin sentencia de import:
| Nombre | Tipo | Qué es |
|---|---|---|
React | módulo | React completo (useState, useEffect, useRef, useMemo, useCallback, useLayoutEffect, Fragment, ...) |
useYumina | hook | SDK de la plataforma — consulta SDK useYumina() |
useAssetFont | hook | Carga una fuente personalizada desde la biblioteca de Recursos — consulta useAssetFont() |
Icons | objeto | Más de 1400 componentes de iconos Lucide: <Icons.Heart />, <Icons.Sword />. Catálogo completo: https://lucide.dev/icons |
Chat | componente | Bloque de construcción completo de chat — consulta <Chat> |
MessageList | componente | Mensajes sin entrada — consulta <MessageList> |
MessageInput | componente | Solo barra de entrada — consulta <MessageInput> |
ChatCanvas | componente | Alias heredado de <Chat /> — consulta <ChatCanvas> |
exports, module | objeto | Fallback de exportación estilo CJS; normalmente los ignoras |
NO importes React ni ninguno de los nombres anteriores: son inyectados por el sandbox. Escribir import React from "react" se elimina silenciosamente en tiempo de compilación pero es redundante.
Tus propios archivos SÍ pueden importarse: los root components multi-archivo usan sintaxis de módulo ES: import StatBar from "./stat-bar". Las extensiones .tsx, .ts, .jsx, .js pueden omitirse.
SDK useYumina()
Llámalo dentro de tu función de componente:
function MyWorld() {
const api = useYumina()
// api.variables, api.sendMessage(...), ...
}Superficie completa, agrupada por propósito:
Lecturas de estado (síncronas)
Lee el último estado del juego. El componente se re-renderiza cada vez que cualquiera de estos cambia.
| Campo | Tipo | Significado |
|---|---|---|
variables | Record<string, unknown> | Variables del juego con alcance de Sesión. Ejemplo: { health: 80, gold: 150 } |
globalVariables | Record<string, unknown> | Variables globales compartidas entre todas las Sesiones |
worldName | string | Nombre del Mundo actual |
worldId | string | UUID del Mundo actual |
sessionId | string | UUID de la Sesión de juego actual |
currentUser | { id, name?, image? } | null | Cuenta cruda: id, nombre visible, avatar de cuenta. null cuando no hay sesión iniciada. Úsalo para UI a nivel de cuenta como "ver perfil". Para renderizado de roleplay dentro del Mundo, usa user en su lugar |
user | { name: string; avatar: string | null } | El jugador interpretado: misma ramificación persona-vs-cuenta que la macro . Cuando el jugador tiene una persona activa, user.name es el nombre de la persona y user.avatar es el avatar de la persona; de lo contrario recurre a la cuenta. Esto es lo que quieres para burbujas de chat dentro del Mundo, Tarjetas de personaje, paneles de perfil |
mode | "session" | "guest-preview" | "session" es una Sesión de juego real. "guest-preview" es una vista previa de hub sin sesión iniciada: las acciones que mutan el estado son no-ops y muestran un prompt de inicio de sesión al padre |
capabilities | { canSendMessage, canPersistSession, canUseSessionApis, requiresAuth } | Lo que permite el mode actual. Léelo para deshabilitar botones que serían no-ops (por ejemplo, el botón Enviar en vista previa de invitado), o para renderizar un CTA inline "Inicia sesión para continuar" |
language | string | Código de idioma i18n activo del host ("en", "zh", ...). Úsalo para elegir traducciones dentro de la Tarjeta sin depender de la instancia i18next del host |
messages | Array<Record<string, unknown>> | Historial completo de mensajes — consulta SandboxMessage |
isStreaming | boolean | true mientras la IA está generando una respuesta |
streamingContent | string | Texto de streaming en vivo de la IA (se actualiza frecuentemente) |
streamingReasoning | string | Texto en vivo de "pensamiento" / razonamiento de la IA (solo para Modelos de razonamiento) |
pendingChoices | string[] | Etiquetas de botones de elección emitidas por reglas |
error | string | null | Mensaje de error actual (fallo de API, error de generación) o null |
readOnly | boolean | true al ver la Sesión de otra persona — <Chat /> oculta la entrada automáticamente |
checkpoints | Array<Checkpoint> | Puntos de control guardados — consulta Checkpoint |
greetingContent | string | null | Texto de saludo calculado a partir de las Entradas del Mundo (usado por <Chat /> como contenido de estado vacío) |
canvasMode | "chat" | "custom" | "fullscreen" | Modo de canvas actual |
selectedModel | string | ID del Modelo de IA actualmente seleccionado |
userPlan | string | Plan de suscripción del usuario ("free", "go", "plus", "pro", "ultra", "internal") |
preferredProvider | "official" | "private" | API oficial vs. clave propia del usuario |
entries | ReadonlyArray<SandboxEntry> | Entradas del Lorebook del Mundo: solo las habilitadas, ordenadas por position. Consulta Búsquedas del Lorebook y SandboxEntry |
Acciones de juego (fire-and-forget)
Estos métodos no devuelven nada; solo envían la intención a la aplicación padre.
| Método | Qué hace |
|---|---|
sendMessage(text) | Envía un mensaje como jugador, activando una respuesta de IA |
setVariable(id, value, options?) | Establece una Variable. options: { scope?: string; targetUserId?: string }. scope elige el alcance de la Variable (para global/personal), targetUserId te permite escribir Variables para un jugador específico en multijugador |
executeAction(actionId) | Dispara una acción nombrada definida por el motor de reglas (ej. "attackBoss") |
switchGreeting(index) | Cambia a una variante de saludo diferente por índice |
clearPendingChoices() | Descarta los botones de elección pendientes sin elegir uno |
setComposerDraft(text) | Suelta text en el compositor de chat y lo enfoca. No envía. Úsalo cuando quieras que el jugador revise o edite el mensaje antes de presionar Enviar (ej. un botón de interacción con NPC que prepara un iniciador de conversación). Local al sandbox — sin ida y vuelta con el padre — así que solo funciona junto con los componentes <MessageInput> / <Chat> incluidos |
Control de chat
Todo lo que puede hacer la barra de chat por defecto, expuesto para que tu UI personalizada también pueda hacerlo.
| Método | Qué hace |
|---|---|
editMessage(messageId, content) | Edita un mensaje existente. Devuelve Promise<boolean>; true en éxito |
deleteMessage(messageId) | Elimina un mensaje. Devuelve Promise<boolean> |
regenerateMessage(messageId) | Pide a la IA que regenere la respuesta dada (fire-and-forget) |
continueLastMessage() | Continúa generando desde el último mensaje de IA (fire-and-forget) |
stopGeneration() | Interrumpe el stream actual (fire-and-forget) |
restartChat() | Limpia todos los mensajes, restablece el estado, empieza de nuevo |
swipeMessage(messageId, "left" | "right") | Cambia entre alternativas de IA (swipes) para un mensaje. Devuelve Promise<Record<string, unknown>> |
Sesiones y ramificación
| Método | Qué hace |
|---|---|
revertToMessage(messageId) | Rebobina la conversación a justo antes de messageId. Devuelve Promise<void> |
branchFromMessage(messageId) | Bifurca una nueva Sesión en el mensaje dado (clona los mensajes hasta él inclusive más la instantánea de estado). Devuelve Promise<string | null> — nuevo ID de Sesión, o null en caso de fallo (mientras se hace streaming, salas multijugador, mensajes faltantes, todo falla) |
getBranchContext() | Obtiene el slice de rama actual (self, parent, siblings, children). Devuelve Promise<BranchContext>. Se vuelve a obtener cada llamada; sin caché de cliente. Consulta BranchContext |
createSession(worldId) | Inicia una nueva Sesión para un Mundo. Devuelve Promise<string> con el nuevo ID de Sesión |
deleteSession(sessionId) | Elimina una Sesión. Devuelve Promise<void> |
listSessions(worldId) | Lista todas las Sesiones de un Mundo. Devuelve Promise<Array<Record<string, unknown>>> |
Puntos de control
Un punto de control es una instantánea con nombre dentro de la Sesión actual a la que puedes rebobinar.
| Método | Qué hace |
|---|---|
saveCheckpoint() | Guarda el estado actual de la Sesión como un nuevo punto de control. Devuelve Promise<void> (el campo checkpoints se actualiza después) |
loadCheckpoints() | Pide al padre que refresque el array checkpoints. Devuelve Promise<void> |
restoreCheckpoint(checkpointId) | Restaura la Sesión a un punto de control guardado. Devuelve Promise<void> |
deleteCheckpoint(checkpointId) | Elimina un punto de control. Devuelve Promise<void> |
Audio
| Método | Qué hace |
|---|---|
playAudio(trackId, opts?) | Reproduce una pista de audio definida en las Entradas. opts: { volume?, fadeDuration?, chainTo?, maxDuration?, duckBgm?, loop? } — las duraciones están en segundos; chainTo elige el siguiente trackId a reproducir; duckBgm baja el BGM durante la reproducción; loop sobrescribe el ajuste loop de la pista solo para esta reproducción |
stopAudio(trackId?, fadeDuration?) | Detiene una pista (omite trackId para detener todo). fadeDuration en segundos. Destruye el elemento — usa pauseAudio si quieres reanudar desde la misma posición |
pauseAudio(trackId) | Pausa una pista en el sitio, conservando su posición de reproducción |
resumeAudio(trackId) | Reanuda una pista pausada con pauseAudio |
onAudioEnded(cb) | Suscríbete a "una pista no en bucle terminó de reproducirse" — cb(trackId). Devuelve una función para cancelar la suscripción. Úsalo para avanzar automáticamente una lista de reproducción personalizada |
setAudioVolume(type, volume) | type es "bgm" o "sfx", volume es 0-1 |
getAudioVolume(type) | Devuelve síncronamente el volumen actual (0-1) |
UI / navegación
| Método | Qué hace |
|---|---|
toggleImmersive() | Alterna el modo inmersivo (pantalla completa) |
openPersonaManager() | Abre el gestor de personas del jugador — cambiar / crear / editar personas sin salir del mundo (el mismo panel que abre el menú "+" del compositor). La persona activa es global de la cuenta; un cambio surte efecto en el siguiente mensaje |
copyToClipboard(text) | Copia al portapapeles (reemplaza navigator.clipboard.writeText) |
navigate(path) | Pide al padre que rute a una ruta como "/app/hub" (reemplaza window.location = ...) |
showToast(message, type?) | Muestra un toast en la UI del padre. type: "success", "error", "info" (por defecto) |
Almacenamiento persistente (por Mundo)
Reemplazo de localStorage. Con alcance por worldId; los Mundos no pueden leer las claves de los demás.
| Método | Qué hace |
|---|---|
storage.get(key) | Lee. Devuelve Promise<string | null> |
storage.set(key, value) | Escribe (solo strings). Devuelve Promise<void> |
storage.remove(key) | Elimina. Devuelve Promise<void> |
¿Necesitas datos complejos? JSON.stringify / JSON.parse al entrar/salir.
Búsquedas del Lorebook
Acceso de solo lectura al Lorebook del Mundo desde dentro de tu Tarjeta. Útil para inspeccionar o seleccionar manualmente Entradas al ensamblar un prompt de LLM lateral, construir un visor de diario dentro del juego o conectar un panel de depuración.
| Campo / método | Qué hace |
|---|---|
entries | ReadonlyArray<SandboxEntry> — cada Entrada habilitada, ya ordenada por position. Consulta SandboxEntry |
getEntry(name) | Busca una Entrada por nombre exacto. Devuelve SandboxEntry | null. En localhost, una búsqueda fallida registra una advertencia única con los nombres disponibles: útil cuando renombras una Entrada y olvidas actualizar la Tarjeta |
Para la mayoría de los casos no necesitas tocar estos directamente: pasa includeLorebook: "matched" a ai.complete() y el servidor ensambla el lore por ti (consulta más abajo). Recurre a entries / getEntry cuando necesites control quirúrgico — por ejemplo, "este NPC solo conoce Entradas etiquetadas con tavern".
Completaciones crudas de IA
Llama al LLM fuera de la tubería principal del chat. Úsalo para "monólogo interno de NPC en un panel lateral", "descripciones de elementos generadas por IA", "chats telefónicos dentro de la Tarjeta", etc. No escribe en el historial de mensajes, no activa actualizaciones de estado, no consume saludos.
const api = useYumina()
const text = await api.ai.complete({
messages: [
{ role: "system", content: "You are a surly merchant." },
{ role: "user", content: "Price me an iron sword." },
],
onDelta: (chunk) => setStreaming((s) => s + chunk), // optional, per-token
model: "claude-sonnet-4-6", // optional, defaults to selectedModel
maxTokens: 500, // optional, default 2048, max 8192
temperature: 0.7, // optional
includeLorebook: "matched", // optional — see below
})Devuelve Promise<string> con la respuesta completa. Timeout del lado del cliente de 120 segundos.
Límites y costos
| Límite | Valor | Fuente |
|---|---|---|
| Máx. mensajes por llamada | 50 | El servidor rechaza con HTTP 400 |
| Máx. contenido total | 50.000 caracteres entre todos los mensajes | El servidor rechaza con HTTP 400 |
maxTokens por defecto | 2048 | Por defecto cuando se omite |
maxTokens techo | 8192 | Los valores mayores se recortan silenciosamente |
Rango temperature | 0-2, por defecto 1.0 | Los valores fuera de rango se recortan |
| Modelo por defecto | El selectedModel del jugador, recurriendo a anthropic/claude-sonnet-4.6 si ni model ni selectedModel están establecidos | |
| Límite de tasa | Compartido con el chat principal — las llamadas laterales y los turnos del chat principal cuentan contra el mismo presupuesto por minuto | Devuelve HTTP 429 + código estilo INSUFFICIENT_CREDITS en desbordamiento |
| Créditos | Misma facturación por token que el chat principal. Los usuarios BYOK omiten la deducción de créditos del servidor pero aún pagan a su propio proveedor | Registrado con endpoint "side-completion" |
| Autenticación | La Sesión debe pertenecer al jugador actual; de lo contrario, la llamada falla con HTTP 404 |
includeLorebook — auto-inyectar lore del Mundo
Las llamadas laterales eluden el ensamblaje del prompt del chat principal, por lo que el Modelo no tiene idea de quiénes son tus personajes a menos que le des el Lorebook. Pasa includeLorebook y el servidor antepone un mensaje del sistema construido a partir de las Entradas del Mundo:
| Valor | Comportamiento |
|---|---|
omitido / false | Sin inyección (por defecto). Úsalo para traducciones, resúmenes, clasificación: cualquier cosa que no necesite contexto del Mundo |
true / "all" | Inyecta cada Entrada habilitada no-greeting, ordenada por position. Predecible, mayor costo de tokens |
"matched" | Ejecuta el mismo matcher de palabras clave que usa el chat principal contra el último mensaje del usuario en messages. Las Entradas alwaysSend siempre se incluyen; las Entradas activadas por palabras clave se añaden solo cuando son relevantes. Recomendado para llamadas laterales en personaje |
Sin esto, una llamada lateral "en personaje" tiene al Modelo fabricando personalidades solo a partir de los nombres: la persona dentro de la Tarjeta se aleja del chat principal. Con "matched", un chat telefónico con un NPC ve el mismo lore del Mundo + perfil de personaje que ve el chat principal.
// A phone chat that stays in canon
api.ai.complete({
messages: [
{ role: "system", content: "Stay strictly in character as Balder. Reply in one or two short lines." },
...history,
{ role: "user", content: userText },
],
includeLorebook: "matched", // server pulls Balder's profile + relevant world lore
})Si necesitas control más fino —inyectar una Entrada específica por nombre, o solo Entradas con una Etiqueta específica— itera sobre api.entries y ensambla el mensaje del sistema tú mismo en lugar de usar includeLorebook:
const tavernLore = api.entries
.filter((e) => e.tags?.includes("tavern"))
.map((e) => `【${e.name}】\n${e.content}`)
.join("\n\n")
api.ai.complete({
messages: [
{ role: "system", content: `You are the tavern keeper.\n\n${tavernLore}` },
{ role: "user", content: userText },
],
})Advertencias del modo "matched": solo escanea el último mensaje del usuario en busca de palabras clave (no todo el historial), y las Entradas con compuerta condicional que dependen de Variables del juego no se disparan en llamadas laterales (el matcher ve un stub de estado vacío). Usa true para forzar la inclusión de todo si la precisión importa más que los tokens.
Inyección de contexto
Inyecta un mensaje de contexto de un solo uso en el próximo turno de IA del chat principal. Se consume después de un uso; no se crea ningún mensaje de chat visible. Genial para "mensajes telefónicos", "diálogo de NPC fuera de escena", "cambios ambientales": cosas que la IA principal debería saber pero que el jugador no debería ver como una burbuja de chat.
api.injectContext("You just received a cryptic text: 'Tonight, 9pm, usual place.'", { role: "system" })
// En el próximo mensaje del jugador, la IA principal verá esto como un mensaje de sistema.options: { role?: "system" \| "user" } (por defecto "system").
Selector de modelo
| Campo / método | Qué hace |
|---|---|
selectedModel | ID del Modelo actual |
userPlan | Nivel de plan del usuario |
preferredProvider | "official" o "private" |
setModel(modelId) | Cambia de Modelo (fire-and-forget) |
getModels() | Devuelve Promise<{ models, pinnedModels, recentlyUsed }> donde models es Array<{ id, name, provider, contextLength }> |
pinModel(modelId) / unpinModel(modelId) | Fija / desfija un Modelo |
Recursos
| Método | Qué hace |
|---|---|
resolveAssetUrl(ref) | Convierte una referencia @asset:xxx en una URL de CDN. Transformación pura de string, sin red. Las URLs HTTP/HTTPS pasan sin cambios |
Markdown
| Método | Qué hace |
|---|---|
renderMarkdown(text) | Convierte markdown en HTML seguro (entidades HTML escapadas, etiquetas peligrosas eliminadas, formato preservado). Pasa el resultado a dangerouslySetInnerHTML dentro de una burbuja personalizada y estás seguro — consulta el ejemplo a continuación |
<div dangerouslySetInnerHTML={{ __html: api.renderMarkdown(msg.rawContent) }} />Componentes
<Chat>
La experiencia completa de chat de la plataforma. Este es el bloque de construcción cotidiano: cero props te da el chat por defecto.
Incluye: lista de mensajes, auto-scroll, cursor de streaming, controles de swipe, acciones de mensaje (editar/eliminar/regenerar), barra de entrada, botones de elección, selector de Modelo, modo de solo lectura, placeholder de saludo.
<Chat renderBubble={(msg) => <MyBubble {...msg} />} />Props
| Prop | Tipo | Descripción |
|---|---|---|
renderBubble? | (props: BubbleProps) => ReactNode | Personaliza cómo se ve cada burbuja de mensaje. Recurre al renderizado de markdown por defecto si se omite |
className? | string | Clase CSS extra en el contenedor exterior |
children? | ReactNode | Contenido renderizado arriba de la lista de mensajes (ej. una cabecera HUD fija) |
BubbleProps
El objeto msg que tu callback renderBubble recibe:
| Campo | Tipo | Significado |
|---|---|---|
contentHtml | string | HTML seguro pre-renderizado (markdown ya convertido). Normalmente canalizado a dangerouslySetInnerHTML |
rawContent | string | Texto markdown crudo antes del renderizado (texto de Directiva incluido) |
role | "user" | "assistant" | "system" | Origen del mensaje |
messageIndex | number | Posición en la lista (0 = primero, normalmente el saludo) |
isStreaming | boolean | true mientras este mensaje se está streameando |
stateSnapshot | Record<string, unknown> | null | Estado del juego en el momento en que se generó este mensaje (útil para "cuáles eran HP/location entonces") |
variables | Record<string, unknown> | Variables actuales (más recientes) del juego |
renderMarkdown | (text) => string | Helper: convierte cualquier texto markdown en HTML seguro |
<MessageList>
Solo el stream de mensajes (con scroll, cursor de streaming, controles de swipe). Sin barra de entrada.
<MessageList />No acepta renderBubble: para personalizar burbujas usa <Chat renderBubble={...} />, o salta <MessageList> por completo y lee api.messages directamente (el patrón de novela visual).
<MessageInput>
Solo la barra de entrada (con selector de Modelo, botones de elección, menú continuar/reiniciar, estado de streaming).
<MessageInput />Se oculta automáticamente cuando api.readOnly es true.
<ChatCanvas>
Alias heredado: idéntico a <Chat />. Los Mundos antiguos siguen funcionando; el código nuevo debería preferir <Chat />.
useAssetFont()
Carga un Recurso de fuente subido como un @font-face y obtén de vuelta un string listo para soltar en un valor font-family de CSS.
const fontFamily = useAssetFont("@asset:my-font-id", {
family: "Cinzel",
fallback: "serif",
})
return <div style={{ fontFamily }}>Ancient runes</div>Firma
useAssetFont(
assetRef: string | null | undefined,
options?: AssetFontOptions
): stringLa fuente se carga asíncronamente. Mientras carga, el hook devuelve options.fallback (por defecto "serif"); cuando está lista, se dispara un re-renderizado con el string completo de family (con sufijo para evitar choques de nombres).
AssetFontOptions
| Campo | Tipo | Descripción |
|---|---|---|
family? | string | Nombre de la familia de fuentes. Inferido del nombre de archivo o assetRef si se omite |
fallback? | string | Fuente de respaldo mostrada durante la carga. Por defecto "serif" |
filename? | string | null | Nombre de archivo original, usado para adivinar el formato |
mimeType? | string | null | Tipo MIME, usado para adivinar el formato |
format? | "opentype" | "truetype" | "woff" | "woff2" | null | Anulación explícita del formato |
weight? | string | number | font-weight |
style? | string | font-style (ej. "italic") |
stretch? | string | font-stretch |
display? | FontDisplay | font-display (por defecto "swap") |
Tipos
SandboxMessage
Forma de cada Entrada en api.messages:
interface SandboxMessage {
id: string
sessionId: string
role: "user" | "assistant" | "system"
content: string
status?: "complete" | "streaming" | "failed"
errorMessage?: string | null
authorUserId?: string | null // quién lo envió (multijugador)
authorNameSnapshot?: string | null // su nombre visible en el momento del envío
stateChanges?: Record<string, unknown> | null // diff de actualizaciones de variables de este mensaje
stateSnapshot?: Record<string, unknown> | null // estado completo en el momento de la generación del mensaje
swipes?: Array<{ content, stateSnapshot }> // respuestas alternativas de la IA
activeSwipeIndex?: number
model?: string | null
tokenCount?: number | null
generationTimeMs?: number | null
compacted?: boolean // oculto en la sección "older messages"
attachments?: Array<{ type, mimeType, name, url }> | null
createdAt: string // ISO-8601
}Checkpoint
interface Checkpoint {
id: string
name: string
messageCount: number
createdAt: string // ISO-8601
}SandboxEntry
Una sola Entrada de Lorebook de solo lectura, expuesta mediante api.entries y api.getEntry():
interface SandboxEntry {
id: string
name: string
content: string
keywords: string[]
position: number
section: "system-presets" | "examples" | "chat-history" | "post-history"
enabled: boolean
role: string // "system" | "character" | "lore" | etc.
tags?: string[]
}Esta es una vista delgada del WorldEntry interno del motor: solo los campos que una Tarjeta necesita para el ensamblaje del prompt. El tiempo de ejecución pre-filtra las Entradas deshabilitadas y pre-ordena por position, así que las Tarjetas nunca necesitan hacer ninguna de las dos cosas ellas mismas.
BranchContext
interface BranchNode {
id: string
name: string | null
parentSessionId: string | null
branchedFromMessageId: string | null
messageCount: number
updatedAt: string // ISO-8601
createdAt: string // ISO-8601
}
interface BranchContext {
current: BranchNode // la sesión en la que estás
parent: BranchNode | null // la rama de la que te separaste, o null en la raíz
siblings: BranchNode[] // otras ramas separadas del mismo padre, las más antiguas primero
children: BranchNode[] // ramas separadas de `current`, las más antiguas primero
}APIs de navegador bloqueadas
Tu código se ejecuta dentro de un iframe sandbox="allow-scripts" de origen cruzado sin allow-same-origin. Eso significa:
- Sin acceso a cookies / localStorage de la aplicación padre
- Sin peticiones de red con credenciales
- Sin manipulación directa de
window.parent
Las siguientes APIs están completamente bloqueadas o redirigidas de forma transparente mediante el puente del SDK.
Redirecciones (el código heredado sigue funcionando)
| Lo que escribiste | Lo que realmente sucede |
|---|---|
fetch('/api/...') | Proxied mediante el fetch autenticado del padre |
fetch('/cdn/...') | Permitido (la CSP lo permite) |
fetch('any other URL') | Rechazado (lanza error) |
localStorage.getItem/setItem/removeItem/clear | Enrutado mediante api.storage, con alcance por Mundo |
sessionStorage.* | Igual |
navigator.clipboard.writeText() | Equivalente a api.copyToClipboard() |
navigator.clipboard.readText() / read() / write() | Rechazado (lanza error) |
window.location.pathname / href / assign / replace | Objeto sintético; pathname siempre es /app/chat/{sessionId}; asignar / llamar assign / replace activa la navegación |
window.location.reload() | Enlazado para recargar la Sesión |
window.__yuminaToggleImmersive() | Equivalente a api.toggleImmersive() |
Uso preferido
Al escribir código nuevo, usa el SDK directamente: las redirecciones existen para Mundos antiguos, pero el SDK es más limpio y estable:
| No escribas | Escribe |
|---|---|
fetch('/api/sessions', { method: 'POST' }) | api.createSession(worldId) |
fetch('/api/sessions/' + sid, { method: 'DELETE' }) | api.deleteSession(sid) |
localStorage.getItem("k") | await api.storage.get("k") |
window.location = "/app/hub" | api.navigate("/app/hub") |
navigator.clipboard.writeText(t) | api.copyToClipboard(t) |
APIs de navegador que SÍ están disponibles
El sandbox es permisivo con cualquier cosa que no alcance la red o el origen compartido. Las siguientes funcionan como en cualquier navegador, sin necesidad de wrapper del SDK:
| API | Uso típico en Tarjetas |
|---|---|
<input type="file"> + FileReader.readAsDataURL / readAsText | Deja que el jugador elija un archivo de imagen/audio/texto → almacénalo como URL de datos o string en una Variable. Consulta Receta: Imágenes subidas por el jugador |
URL.createObjectURL / revokeObjectURL | Genera una URL temporal en memoria para un Blob (ej. vista previa antes de guardar) |
<canvas> + getContext("2d") + toDataURL / toBlob | Redimensiona, recorta o compone imágenes antes de guardarlas en una Variable |
<img>, <audio>, <video> | Renderiza URLs de origen local, URLs @asset:... resueltas, URLs data:/blob: |
IntersectionObserver, ResizeObserver, matchMedia, requestAnimationFrame | Primitivas estándar de diseño / animación |
crypto.randomUUID, crypto.subtle | Hashing y generación de IDs para estado del lado del cliente |
WebAudio (AudioContext) | Síntesis o análisis de audio ligero |
Notification, navigator.vibrate, screen.orientation | Limitado por permisos a nivel del navegador, no por el sandbox en sí |
De un vistazo: toda la API
Una tabla, escanea una vez.
useYumina()
├── State reads
│ ├── variables, globalVariables, personalVariables, roomPersonalVariables
│ ├── worldName, worldId, sessionId
│ ├── currentUser (account), user (persona-aware)
│ ├── room, mode, capabilities, language
│ ├── messages, permissions, entries
│ ├── isStreaming, streamingContent, streamingReasoning
│ ├── pendingChoices, error, readOnly, greetingContent, canvasMode
│ ├── checkpoints
│ └── selectedModel, userPlan, preferredProvider
├── Game actions
│ ├── sendMessage(text)
│ ├── setVariable(id, value, options?)
│ ├── executeAction(actionId)
│ ├── switchGreeting(index)
│ ├── clearPendingChoices()
│ └── setComposerDraft(text) // prefill, no send
├── Chat control
│ ├── editMessage(id, content) → Promise<boolean>
│ ├── deleteMessage(id) → Promise<boolean>
│ ├── regenerateMessage(id)
│ ├── continueLastMessage()
│ ├── stopGeneration()
│ ├── restartChat()
│ └── swipeMessage(id, direction) → Promise
├── Sessions / branching
│ ├── revertToMessage(id) → Promise<void>
│ ├── branchFromMessage(id) → Promise<string | null>
│ ├── getBranchContext() → Promise<BranchContext>
│ ├── createSession(worldId) → Promise<string>
│ ├── deleteSession(id) → Promise<void>
│ └── listSessions(worldId) → Promise<Array>
├── Checkpoints
│ ├── saveCheckpoint() → Promise<void>
│ ├── loadCheckpoints() → Promise<void>
│ ├── restoreCheckpoint(id) → Promise<void>
│ └── deleteCheckpoint(id) → Promise<void>
├── Audio
│ ├── playAudio(trackId, opts?)
│ ├── stopAudio(trackId?, fadeDuration?)
│ ├── pauseAudio(trackId)
│ ├── resumeAudio(trackId)
│ ├── onAudioEnded(cb) → unsubscribe
│ ├── setAudioVolume(type, volume)
│ └── getAudioVolume(type) → number
├── UI / navigation
│ ├── toggleImmersive()
│ ├── openPersonaManager()
│ ├── copyToClipboard(text)
│ ├── navigate(path)
│ └── showToast(message, type?)
├── Storage
│ ├── storage.get(key) → Promise<string | null>
│ ├── storage.set(key, value) → Promise<void>
│ └── storage.remove(key) → Promise<void>
├── Lorebook
│ ├── entries (ReadonlyArray<SandboxEntry>) // sorted by position, enabled only
│ └── getEntry(name) → SandboxEntry | null
├── AI
│ └── ai.complete({ messages, onDelta?, model?, maxTokens?, temperature?, includeLorebook? }) → Promise<string>
│ // includeLorebook: true | "all" | "matched" — auto-inject world lore
├── Context injection
│ └── injectContext(message, { role? })
├── Model picker
│ ├── setModel(modelId)
│ ├── getModels() → Promise<{ models, pinnedModels, recentlyUsed }>
│ ├── pinModel(id), unpinModel(id)
├── Assets
│ └── resolveAssetUrl(ref) → string
└── Markdown
└── renderMarkdown(text) → string // safe HTML
Sandbox globals (no import)
├── React
├── useYumina, useAssetFont
├── Icons (1400+ Lucide icons)
├── Chat, MessageList, MessageInput, ChatCanvas (legacy alias)
└── Tailwind utility classes (CSS-level)
Blocked / redirected
├── fetch('/api/...') → proxied
├── localStorage / sessionStorage → api.storage
├── window.location → synthetic + navigate
└── navigator.clipboard → copyToClipboard
Browser APIs that work as-is
├── <input type="file"> + FileReader // player file uploads → data URL
├── <canvas>, URL.createObjectURL // image processing
├── IntersectionObserver, ResizeObserver, matchMedia, rAF
├── crypto.randomUUID, crypto.subtle
└── WebAudio (AudioContext)Siguiente: vuelve a la Guía de UI personalizada para ejemplos elaborados, o navega las Recetas para plantillas más cercanas a lo que estás construyendo.
