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
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.postextimportado, 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 postextimport { 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
| Prop | Tipo | Descripción |
|---|---|---|
initialMarkdown | string | Contenido markdown inicial |
initialConfig | PostextConfig | Configuración de maquetación inicial |
className | string | Clase CSS para el contenedor raíz |
labels | Partial<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 |
locale | string | Idioma 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 |
hashBundles | Record<string, (id: string, lang: string | null) => string | string[] | null> | Libros enlazados con una clave de fragmento propia del integrador: { recipe: (slug, lang) => 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) => void | Callback cuando cambia la configuración |
onMarkdownChange | (markdown) => void | Callback cuando cambia el markdown |
webmcp | boolean | Registra las herramientas WebMCP del Sandbox para agentes en el navegador mientras está montado. Por defecto true |
themeToggle | ReactNode | Componente personalizado para el conmutador de tema, alojado en la parte inferior de la barra de actividad |
languageSwitcher | ReactNode | Componente personalizado para el selector de idioma, alojado en la parte inferior de la barra de actividad |
homeUrl | string | Si se establece, renderiza un enlace con el logo de inicio en la parte superior de la barra de actividad apuntando a esta URL |
homeLink | ReactNode | Nodo 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,#2b4acby#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
iddepreset.jsondebe coincidir con la entrada deindex.json;description,localeydefaultdel manifiesto prevalecen sobre los valores del índice una vez cargado el preset, igual que los campos de escaparatelocales,openLocale,license,creditsytags.openLocale, uno de loslocales, 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 紅樓夢 conzh-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 delocales(olocale) y la licencia, los créditos bajo la descripción, ythumbnail— 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 deapps/web/public/presets/usan todos los campos y llevan unCREDITS.mdcon la licencia y la procedencia de cada texto e imagen. configVersion(opcional) nombra las reglas de configuración para las que se escribieronconfigy las configuraciones delocalized. 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 lacontinuationde 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 delocalizedpuede llevar unstartpropio 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 elconfigcompartido (las claves de primer nivel se sustituyen enteras — típicamenteresourceTypescon los prefijos de pie traducidos) y sobreresources(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 propiaview, 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 quezh-TWleezh-Hantantes quezh-Hans; luego el mismo idioma en la otra escritura, de modo que quien pidezh-TWa un paquete con la clavezha secas recibe el chino; luego ellocaledel propio preset y, por último, la primera entrada), y solo se leen los archivos de capítulo de ese idioma. Untitlevacío se deriva del primer título de nivel 1 del archivo. El sandbox siempre escribe la versión 2.width/heightson opcionales para los recursossvgybitmap— si faltan, se leen del archivo al cargar. Un mapa de bits también puede llevarresolution(sus ppp a su tamaño natural de impresión) yfileResolution(la que indica su archivo, leída al cargar cuando la configuración fijalayout.bitmapResolution: 'file').pdfFile(solo recursossvg) 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 textostext/tspancompuestos 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
textde 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 enfont-familyexactamente como aparece en Fuentes. Un recurso puede excluirse con"inlineFonts": falseen 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.modeles unTableModelde Postext:rowses un array de celdas por filas (contenty, opcionalmente,colSpan,rowSpan,isHeader,align,verticalAlign),headerRowCountel número de filas de cabecera iniciales ycolumnWidthsun array opcional de pesos relativos de columna.table.styleIdnombra, opcionalmente, uno de lostableStylesde 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.customFontsal aplicar el preset; unindex.jsonausente o mal formado no produce ningún preset en lugar de un error. <directorio-del-preset>/fingerprint.jsones opcional y, en la ruta de desarrollo, virtual: cuando no existe tal archivo,GETresponde{ "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 ynode_modules), nunca a partir de su contenido, conCache-Control: no-store. El sandbox lo consulta para seguir las ediciones del paquete (consulta Panel de libros). Un servidor estático puede servir unfingerprint.jsonreal con la misma forma, o ninguno, en cuyo caso sus presets no se siguen.