Saltar al contenido principal

Capítulo 12.4 · Parte III · La práctica

Sandbox: agentes, incrustación y presets

Las herramientas WebMCP para agentes de IA, el componente PostextSandbox dentro de tu aplicación con sus props y su CSS, y el formato de paquete de preset

Actualizado 2026-10-1613 minEn el capítulo 12 Sandboxenescadefritptzhjaar

En pocas palabras

Esta página es para quienes crean sitios web o usan asistentes de IA. Un asistente que funciona en el navegador puede leer y cambiar el libro del Sandbox con un conjunto de herramientas. Puedes poner el Sandbox dentro de tu propia web como un solo componente, y esta página enumera sus opciones y los estilos que necesita. La última sección describe los archivos de un libro de ejemplo, que el Sandbox puede abrir.

#Agentes: WebMCP

El Sandbox entrega a un agente que trabaje en el navegador sus propias herramientas mediante WebMCP (document.modelContext), de modo que un asistente actúa sobre el libro mismo en lugar de ir pulsando por la interfaz. No hay nada que configurar: las herramientas quedan registradas mientras el Sandbox está en pantalla, y el resto de este sitio registra unas pocas propias en cada página (buscar en la documentación y en el Recetario, leer una página como Markdown, abrir una receta en el Sandbox, cambiar de idioma y de tema).

Están pensadas para el ciclo que un diseñador recorre a ojo: cambiar algo, mirar la página, volver a cambiar.

  • Ver — sandbox_get_state, sandbox_render_page, sandbox_inspect_pages, sandbox_inspect_point, sandbox_inspect_comic, sandbox_get_warnings: el libro con sus capítulos y sus rangos de páginas, y lo que pasa en la interfaz (la selección, un aviso, una pregunta pendiente de respuesta); cualquier página como JPEG, PNG o WebP al ancho pedido, o un recorte para verla de cerca, con la vista previa de impresión si se pide; la misma página como datos (sus bloques y líneas, cuánto se llena cada columna, qué líneas quedan flojas o terminan en palabra partida); lo que hay en un punto de la página (la línea del texto fuente, un enlace y adónde lleva, la celda de una tabla); las viñetas y los bocadillos de una página de cómic; y lo que encuentra la revisión en cualquier capítulo o en el libro entero, sean cuales sean el capítulo y el visor en pantalla.
  • Cambiar — sandbox_set_config, sandbox_get_config, sandbox_find_setting, sandbox_edit_text, sandbox_write_text, sandbox_read_text, sandbox_search_text, sandbox_select_text, sandbox_undo_text, sandbox_manage_chapters, sandbox_save_resource, sandbox_save_font, sandbox_edit_comic, sandbox_import_word: cualquier ajuste por su ruta (bodyText.fontSize, page.margins.top, headings.levels[level=2].fontSize, header.elements[id=titleOdd].italic), el texto mediante buscar y reemplazar exactos, con deshacer, los capítulos, las imágenes, los dibujos SVG, los vídeos, las tablas con su formato, las fuentes, las viñetas y los bocadillos de un cómic y un documento de Word importado con sus estilos asignados. Un cambio responde con cada valor antes y después y con las páginas que ocupa cada capítulo; un ajuste dice además en qué lo resuelve el motor y qué otros ajustes lo han seguido, y un valor que el motor no puede resolver se rechaza sin cambiar nada.
  • Mostrar — sandbox_set_view, sandbox_go_to_page, sandbox_run_viewer: la pestaña del visor, el panel lateral, capítulo o libro entero, el zoom, el ajuste y las páginas enfrentadas, el tamaño de letra del lector, la vista previa de impresión, las guías dibujadas sobre las páginas, la lupa del Folio y la página en pantalla (por su número, por un pasaje, por un ancla o por una entrada del índice de contenidos de un EPUB); volver a generar el PDF o el EPUB, imprimir y reproducir un vídeo de una página.
  • Guardar — sandbox_list_books, sandbox_open_book, sandbox_save_book, sandbox_import_book, sandbox_export, sandbox_word_templates, sandbox_answer: las muestras y tus proyectos con sus portadas, un archivo .postext importado, el libro —o uno que no está abierto— exportado a PDF, EPUB, .postext, Word, Markdown o sus ajustes en JSON, las plantillas de la importación de Word y la respuesta a una pregunta que hace el Sandbox.

Una herramienta que cambia el libro espera a la maquetación, así que la siguiente imagen es siempre la de las páginas nuevas; la página se maqueta y se pinta para la herramienta sea cual sea la pestaña en pantalla. Lo que no se puede deshacer —borrar un capítulo, un recurso o un proyecto, restaurar el original, importar un documento de Word sobre el texto, sustituir tu copia de un libro— va marcado como consecuente, para que el navegador pueda preguntarte antes. Igual que con la interfaz, todo se queda en tu navegador: las herramientas no alcanzan nada que el propio Sandbox no alcance.

WebMCP es un borrador: Chrome lo ofrece como prueba de origen y tras chrome://flags/#enable-webmcp-testing. En un navegador que no lo tenga, las mismas herramientas están en window.postextWebMcp (listTools(), executeTool(name, args)), para un agente que pueda ejecutar código en la página. Al incrustar el Sandbox, webmcp={false} las deja fuera (véase Props).

La página WebMCP explica qué se le puede pedir a un agente y cómo se llama cada herramienta, con ejemplos.

#Uso standalone

El Sandbox está disponible como un paquete React independiente (postext-sandbox) que puede embeberse en cualquier aplicación React:

npm install postext-sandbox postext
import { PostextSandbox } from 'postext-sandbox';
 
function App() {
  return (
    <PostextSandbox
      initialMarkdown="# Hola Mundo"
      initialConfig={{ layout: { layoutType: 'double' } }}
      onMarkdownChange={(md) => console.log(md)}
      onConfigChange={(cfg) => console.log(cfg)}
    />
  );
}

#Props

PropTipoDescripción
initialMarkdownstringContenido markdown inicial
initialConfigPostextConfigConfiguración de maquetación inicial
classNamestringClase CSS para el contenedor raíz
labelsPartial<SandboxLabels>Sustituciones de las etiquetas de la interfaz, combinadas sobre los valores por defecto en inglés — pasa etiquetas traducidas junto con locale al embeber en otro idioma
localestringIdioma de la interfaz ('en' por defecto). Selecciona los tipos de recurso y los recursos de ejemplo localizados (un preset o un archivo en un solo idioma recibe los tipos de recurso de su propio idioma), y proporciona la configuración regional de separación silábica que usan las vistas previas cuando no hay ninguna configurada
presetSources{ url: string; private?: boolean }[]Fuentes de presets remotas, consultadas en orden tras el preset integrado. Cada url es una URL base que sirve un index.json (consulta Formato de paquete de preset); private: true etiqueta los presets de la fuente como privados y hace que su marca default pueda cargarse automáticamente. Las fuentes que no responden simplemente no aportan nada
hashBundlesRecord<string, (id: string, lang: string | null) => string | string[] | null>Libros enlazados con una clave de fragmento propia del integrador: { recipe: (slug, lang) => /cookbook/$&#123;slug&#125;/$&#123;lang&#125;/$&#123;slug&#125;.postext } hace que #recipe=<slug>&lang=es abra ese paquete. Devuelve la URL de un archivo .postext del mismo origen que la página, varias para probar en orden (gana la primera que responde) o null para un id que no se sirve. La primera visita importa el archivo como un proyecto que recuerda el enlace (recipe:<slug>:<lang>); una visita posterior pregunta si abrir ese proyecto, con sus cambios, o sustituirlo por el archivo tal como se sirve ahora. Después el fragmento pasa a ser #project=<id>
onConfigChange(config) => voidCallback cuando cambia la configuración
onMarkdownChange(markdown) => voidCallback cuando cambia el markdown
webmcpbooleanRegistra las herramientas WebMCP del Sandbox para agentes en el navegador mientras está montado. Por defecto true
themeToggleReactNodeComponente personalizado para el conmutador de tema, alojado en la parte inferior de la barra de actividad
languageSwitcherReactNodeComponente personalizado para el selector de idioma, alojado en la parte inferior de la barra de actividad
homeUrlstringSi se establece, renderiza un enlace con el logo de inicio en la parte superior de la barra de actividad apuntando a esta URL
homeLinkReactNodeNodo personalizado para el hueco de inicio, que sustituye al enlace con logo por defecto (tiene prioridad sobre homeUrl)

#Requisitos de CSS

El paquete usa clases utilitarias de Tailwind CSS que hacen referencia a variables CSS personalizadas. La aplicación anfitriona debe proporcionar estas variables:

  • --background, --foreground — Colores base
  • --surface — Color para superficies elevadas; --surface-2 — Filas bajo el puntero y opciones resaltadas (por defecto, --background)
  • --slate — Color para texto atenuado
  • --rule — Color de bordes/divisores; --rule-strong — Filete más marcado para los controles bajo el puntero (por defecto, --slate)
  • --brand — Color de acento: la marca del panel activo, los anillos de foco, los interruptores y las opciones seleccionadas
  • --brand-hover — Color de acento al pasar el puntero
  • --brand-soft — Relleno de acento translúcido detrás de las opciones seleccionadas (por defecto, --surface)
  • --brand-contrast — Botón de un interruptor activado (por defecto, --background)
  • --destructive — Color de las acciones destructivas y los errores
  • --accent-blue — Color de enlace/acento
  • --font-sans — Familia tipográfica de la interfaz
  • --font-mono — Familia monoespaciada (para el editor de código)
  • --font-display, --brand-blue, --brand-gilt — Tipo de letra y colores opcionales de la marca del logo por defecto en la barra de actividad (por defecto, Georgia, #2b4acb y #d8a21a)

El sitio web asigna a --brand el dorado en el tema oscuro y el azul en el claro. Las versiones anteriores leían --gilt y --gilt-hover; el sandbox ya no las usa.

Los popovers, menús y tooltips se renderizan en un portal sobre document.body, así que define las variables en :root (o body), no solo en el elemento que envuelve el sandbox. La compilación de Tailwind de la aplicación anfitriona debe escanear el código fuente del paquete (@source "…/postext-sandbox/src/**/*.tsx" en Tailwind 4) para que se generen las clases utilitarias que usa.

#Formato de paquete de preset

Apéndice. Una fuente de presets es un directorio (servido por HTTP) cuya raíz contiene un index.json que lista directorios de preset; cada directorio contiene un manifiesto preset.json más los archivos de markdown, recursos y fuentes a los que se refiere. Las herramientas de scripts/presets/ del repositorio generan paquetes exactamente con esta forma, y un archivo .postext exportado desde el sandbox es uno de estos directorios comprimido en zip (sin el index.json).

<raíz-de-presets>/
  index.json
  <directorio-del-preset>/
    preset.json
    chapters/              un .md por capítulo, en orden (un manifiesto versión 1 nombra un único documento)
    resources/             figuras: .svg / .png / .jpg / .jpeg / .webp / .gif (+ originales de imprenta .pdf opcionales)
    fonts/                 .otf / .ttf / .woff2, una cara por archivo

#index.json

{
  "version": 1,
  "presets": [
    {
      "id": "acme-journal",          // id estable, [a-z0-9-]
      "dir": "acme-journal",         // nombre del directorio relativo a index.json
      "name": "Acme Journal",
      "description": "Artículo de revista a dos columnas",   // opcional
      "locale": "es",                 // opcional
      "default": true,                // opcional, como máximo uno
      "locales": ["es", "en"],        // opcional: todos los idiomas del paquete
      "openLocale": "es",             // opcional: el idioma en que se abre si no se pide ninguno
      "thumbnail": "thumbnail.jpg",   // opcional: vista previa en el selector
      "license": "CC BY 4.0",         // opcional: se muestra como etiqueta
      "credits": "Textos e imágenes: ESO",  // opcional: una línea bajo la descripción
      "tags": ["revista", "dos-columnas"],  // opcional
      "binding": "left"               // opcional: el borde en que la estantería de la web dibuja el lomo ("right" para un libro encuadernado a la derecha)
    }
  ]
}

#preset.json

{
  "version": 2,
  "id": "acme-journal",
  "name": "Acme Journal",
  "description": "Artículo de revista a dos columnas",       // opcional
  "locale": "es",                                     // opcional
  "default": true,                                    // opcional
  "configVersion": 8,                                 // opcional: las reglas de configuración para las que se escribió `config`
  // Los mismos campos opcionales de escaparate que la entrada del índice:
  // "locales", "openLocale", "thumbnail", "license", "credits", "tags"
 
  // Los capítulos del libro, en orden — o una lista por idioma
  "chapters": [
    { "title": "Introducción", "file": "chapters/01-introduccion.md" },
    { "title": "Métodos", "file": "chapters/02-metodos.md" }
  ],
  // "chapters": { "en": [ … ], "es": [ … ] },
  // Los manifiestos versión 1 usan un único documento y se siguen aceptando:
  // "version": 1, "markdown": "article.md"  (o un mapa idioma → archivo)
 
  // JSON de PostextConfig opcional (página, márgenes, columnas, tipografía...).
  // NO incluyas customFonts aquí: las fuentes vienen del array `fonts` de abajo.
  "config": { },
 
  "resources": [
    {
      "id": "fig-1-1",                 // referenciado desde el markdown (:ref)
      "typeId": "figure",              // id del tipo de recurso (figure, table, ...)
      "kind": "svg",                   // "svg" | "bitmap" | "table"
      "file": "resources/fig-1-1.svg", // solo svg/bitmap, relativo a preset.json
      "width": 240.5,                  // opcional, pt (svg) / px (bitmap)
      "resolution": 300,               // opcional, solo bitmap: ppp a su tamaño natural de impresión
      "pdfFile": "resources/fig-1-1.pdf", // opcional, solo svg: original de imprenta vectorial (ver notas)
      "height": 180,                   // opcional
      "caption": "Texto de la leyenda",
      "altText": "Descripción accesible",             // opcional
      "safeArea": { "x": 0.18, "y": 0.25, "width": 0.5, "height": 0.6 }, // opcional, solo svg/bitmap: la parte que siempre se ve, en fracciones de la imagen
      "placement": { "position": "top", "span": "column" },  // opcional
      "note": "Fuente: ...",                          // opcional
      "table": { "model": { "rows": [[{ "content": "A", "isHeader": true }]], "headerRowCount": 1 } }
                                                      // solo kind "table"
    }
  ],
 
  "fonts": [
    {
      "name": "Some Family",           // nombre de familia usado por la configuración
      "variants": [
        { "weight": 400, "style": "normal", "file": "fonts/SomeFamily-Regular.otf" },
        { "weight": 400, "style": "italic", "file": "fonts/SomeFamily-Italic.otf" },
        { "weight": 700, "style": "normal", "file": "fonts/SomeFamily-Bold.otf" }
      ]
    }
  ]
}

Notas:

  • El id de preset.json debe coincidir con la entrada de index.json; description, locale y default del manifiesto prevalecen sobre los valores del índice una vez cargado el preset, igual que los campos de escaparate locales, openLocale, license, credits y tags. openLocale, uno de los locales, es el idioma en que se abre el libro cuando un enlace o un clic no piden ninguno (la estantería de la portada web también enlaza con él): úsalo cuando el original no está en el idioma del lector, como hace el escaparate de 紅樓夢 con zh-Hant, para que su traducción inglesa no se abra a todo lector de habla inglesa. Sin él decide el idioma de la interfaz. El selector muestra como etiquetas cada idioma de locales (o locale) y la licencia, los créditos bajo la descripción, y thumbnail — una pequeña imagen de página relativa al directorio del preset — en lugar de la columna de la marca de verificación; un libro copiado del preset (Hacer una copia propia) la conserva como portada. Los presets públicos de escaparate de apps/web/public/presets/ usan todos los campos y llevan un CREDITS.md con la licencia y la procedencia de cada texto e imagen.
  • configVersion (opcional) nombra las reglas de configuración para las que se escribieron config y las configuraciones de localized. Todo lo que se escribe desde postext 1.5 lo fija en 8. Un manifiesto sin él lo escribió postext 1.4 o anterior, y sus saltos de encabezado, el tamaño de sus fórmulas, el espacio en torno a sus figuras en línea (en el texto y en los recuadros), las marcas de sus títulos, sus letras capitulares, el sitio bajo una línea de dos puntos, los cortes de sus recuadros, los cortes tras una raya y tras el guion de un compuesto, su texto en bandera, sus divisiones bajo un título y el espacio bajo sus contenedores de párrafos se leen tal como los componía la 1.4 (consulta Saltos de encabezado guardados con postext 1.4 o anterior, Tamaño de las fórmulas guardado antes de postext 1.5, Figuras en línea guardadas antes de postext 1.5, Figuras en línea de los recuadros guardadas antes de postext 1.5, Marcas de los títulos y letras capitulares guardadas antes de postext 1.5, Listas tras dos puntos guardadas antes de postext 1.5, Cortes de los recuadros guardados antes de postext 1.5, Rayas y texto en bandera guardados antes de postext 1.5, Compuestos guardados antes de postext 1.5, Divisiones bajo un título guardadas antes de postext 1.5 y Contenedores de párrafos guardados antes de postext 1.5 en Persistencia); a uno marcado con 3 a 7 (una versión preliminar de la 1.5) solo se le aplican las fijaciones de las reglas posteriores: con 3, el tamaño de las fórmulas, el hueco en línea, las cinco fijaciones de las reglas 6, las dos de las reglas 7 y las tres de las reglas 8; con 4, el hueco en línea y las de las reglas 6, 7 y 8; con 5, las de las reglas 6, 7 y 8; con 6, las de las reglas 7 y 8; con 7, solo las de las reglas 8. Fíjalo en 8 cuando escribas a mano un manifiesto para las reglas actuales.
  • view (opcional) indica cómo abre el sandbox el paquete: { "canvasScope": "book" } maqueta el libro entero en el canvas (consulta Libros y capítulos); si falta, el canvas muestra un capítulo cada vez. Los libros exportados lo llevan cuando está activo. Un libro de más de 200 capítulos se abre de capítulo en capítulo pida lo que pida.
  • start (opcional) indica dónde empieza el libro cuando el paquete contiene parte de uno más largo: { "pageIndexOffset": 57, "pageNumbering": { "startAt": 58 }, "headings": { "h1": 3 } } lo abre en la página 58, par, en el capítulo 4. Es la continuation de su primer capítulo (buildDocument); el sandbox maqueta el libro desde ahí, lo conserva con el libro al guardarlo como proyecto y lo vuelve a escribir al exportar. Ningún panel lo edita. Una entrada de localized puede llevar un start propio para su edición.
  • localized (opcional) contiene las variantes por idioma de un paquete bilingüe: { "es": { "config": { … }, "resources": [ { "id": "fig-1", "caption": "…", "note": "…", "altText": "…" } ] } }. Las variantes del idioma del que se tomaron los capítulos se aplican sobre el config compartido (las claves de primer nivel se sustituyen enteras — típicamente resourceTypes con los prefijos de pie traducidos) y sobre resources (pie, nota, texto alternativo y celdas de tabla, fusionados por id); un idioma sin entrada conserva la redacción compartida. Una entrada puede llevar además su propia view, que se aplica sobre la compartida a esa edición: 紅樓夢 abre sus ediciones chinas en el libro completo y la inglesa de Joly, mucho más lenta de pintar entera, de capítulo en capítulo ("en": { "view": { "canvasScope": "chapter" } }).
  • chapters (versión 2) es una lista de { title, file } en el orden del libro, o un mapa idioma → lista; markdown (versión 1) es un nombre de archivo o un mapa idioma → archivo. En ambos casos el sandbox elige la entrada que coincide con el idioma pedido (etiqueta exacta, sin distinguir mayúsculas; luego el mismo idioma y la misma escritura, de modo que zh-TW lee zh-Hant antes que zh-Hans; luego el mismo idioma en la otra escritura, de modo que quien pide zh-TW a un paquete con la clave zh a secas recibe el chino; luego el locale del propio preset y, por último, la primera entrada), y solo se leen los archivos de capítulo de ese idioma. Un title vacío se deriva del primer título de nivel 1 del archivo. El sandbox siempre escribe la versión 2.
  • width / height son opcionales para los recursos svg y bitmap — si faltan, se leen del archivo al cargar. Un mapa de bits también puede llevar resolution (sus ppp a su tamaño natural de impresión) y fileResolution (la que indica su archivo, leída al cargar cuando la configuración fija layout.bitmapResolution: 'file').
  • pdfFile (solo recursos svg) apunta a un PDF de una página con la misma figura en vectores — normalmente la exportación original de Illustrator o PDF de la que se derivó el SVG. La exportación a PDF incrusta esa página tal cual (fuentes, degradados y espacios de color intactos) en lugar del SVG; las vistas previas en canvas y HTML siguen usando el SVG. Se ignora en modo de una tinta, cuyo recoloreado solo opera sobre el código SVG, y si el archivo falta solo se pierde el original, nunca la figura. Editar el código del SVG en el detalle del recurso quita el original (se hizo para el código tal como estaba), de modo que la exportación a PDF sigue el SVG editado a partir de ahí; restaura el original (Restaurar el original… en el panel Libros) para recuperarlo. Los SVG sin original se emiten como trazados vectoriales cuando se mantienen dentro del subconjunto del backend PDF (formas básicas, trazados, grupos, use, recortes, rellenos y trazos sólidos, opacidad, y textos text / tspan compuestos con las fuentes que el documento puede cargar — primero las fuentes personalizadas, después las familias de Google) y se rasterizan a 600 ppp en caso contrario.
  • El text de un SVG se renderiza con las fuentes del libro en todas partes: las vistas previas y el EPUB incrustan en el SVG las variantes que nombra su texto antes de mostrarlo (un <img> no ve las fuentes de la página), las familias personalizadas tal como se subieron y las de Google como los archivos de Fontsource que contienen sus caracteres, y la exportación a PDF compone los textos como texto real y seleccionable con las mismas fuentes incrustadas. Escribe la familia en font-family exactamente como aparece en Fuentes. Un recurso puede excluirse con "inlineFonts": false en su entrada del manifiesto (svg.inlineFonts).
  • El editor de código del SVG solo permite escribir dentro de los nodos de texto (resaltados) para que una figura no se rompa por accidente; el botón de candado de su cabecera abre todo el código — coordenadas, colores, estructura — y al volver a bloquear se vuelven a detectar los nodos de texto.
  • table.model es un TableModel de Postext: rows es un array de celdas por filas (content y, opcionalmente, colSpan, rowSpan, isHeader, align, verticalAlign), headerRowCount el número de filas de cabecera iniciales y columnWidths un array opcional de pesos relativos de columna. table.styleId nombra, opcionalmente, uno de los tableStyles de la configuración.
  • Los archivos de fuente se guardan en IndexedDB con ids derivados del id del preset y la ruta del archivo, y las familias se añaden a config.customFonts al aplicar el preset; un index.json ausente o mal formado no produce ningún preset en lugar de un error.
  • <directorio-del-preset>/fingerprint.json es opcional y, en la ruta de desarrollo, virtual: cuando no existe tal archivo, GET responde { "fingerprint": "<sha1 hex>", "files": <n> } calculado a partir de los metadatos de los archivos del directorio (ruta relativa, fecha de modificación y tamaño de cada archivo hasta cuatro niveles de profundidad, omitiendo los archivos ocultos y node_modules), nunca a partir de su contenido, con Cache-Control: no-store. El sandbox lo consulta para seguir las ediciones del paquete (consulta Panel de libros). Un servidor estático puede servir un fingerprint.json real con la misma forma, o ninguno, en cuyo caso sus presets no se siguen.