How a JSON or JSON-like language enables the next generation of safe human and AI-generated UIs Introducción en , señalé que el JavaScript crudo generado por el código generado por la IA es un problema de seguridad, y la flexibilidad de JavaScript sin un marco puede resultar en código difícil de administrar. o a (cDOM) con el (JPRX si), si queremos confiar en un LLM para construir nuestras interfaces. El futuro de la UI generada por IA A2o Casa Computadora JSON Pointer Expresiones Regulares También ayuda si el enfoque se basa en los estándares de la industria para los que hay mucha documentación y ejemplos que probablemente han sido consumidos por los LLM durante la formación. cCOM y JPRX hacen esto; incorporan conceptos y sintaxis de JSON Pointers, JSON Schema y XPath. En mi artículo anterior, para mostrar cómo funciona un cDOM y JPRX, usé un contador reactivo, pero vamos a ser reales: los contadores reactivos y las listas de tareas son fáciles. Cualquier marco se ve elegante cuando la lógica se ajusta a una toalla. Para demostrar que un enfoque basado en JSON realmente se mantiene, necesita un problema con el estado confuso, los casos de borde y modos distintos de operación. Necesita una calculadora. Los cálculos son intrínsecamente complicados: Modos de entrada: ¿estamos escribiendo un número nuevo o adjuntando un número existente? Chaining: ¿Qué sucede cuando golpeas '+' y luego '-' y luego '*' sin golpear iguales? DRY Logic: ¿Cómo minimizamos las diferencias de código entre 10 manejadores para botones 0-9? Por lo tanto, le pedí a Claude Opus que construyera una calculadora totalmente funcional de estilo iOS usando Expresiones declarativas cDOM y JPRX. zero custom JavaScript functions El hecho de que la IA pudiera producir una calculadora declarativa con poco impulso puramente de la documentación demuestra otro punto que hice en mi artículo anterior: cDOM y JPRX no son sólo una nueva sintaxis. El Código Para reducir los caracteres y el ruido de la cita al permitir la explicación en línea, estoy usando cDOMC, una versión comprimida de un cDOM. Un cDOM regular no soporta comentarios y requiere citas alrededor de atributos y expresiones JPRX. Cuando se representa con citas y sin comentarios, el cDOM puede ser tratado como JSON normal. { div: { class: "calculator", // A calculator feels stateless, but it's actually a strict state machine. // You're never just "typing a number"; you're either entering the first operand, // waiting for an operator, or entering the next operand. onmount: =state({ display: "0", // What you see on the screen expr: "", // History string, (e.g. "8 + 5 =") prev: "", // value stored before an operation op: "", // the active operator waiting: false // true when expecting a new number vs operator },{ name: "c", // the root name of our state, so we can express things like: /c/display schema: "polymorphic", // allow type changes, e.g. "0" or 0 scope: $this // scope the path to the current element }), children: [ // Display area { div: { class: "display", children: [ { div: { class: "expression",children[=/c/expr] }}, { div: { class: "result",children[=/c/display] }} ] } }, // Button grid { div: { class: "buttons", children: [ // Row 1: AC, ±, %, ÷ { button: { class: "btn btn-clear", onclick: =/c = { display: "0", expr: "", prev: "", op: "", waiting: false }, children: ["AC"] } }, { button: { class: "btn btn-function", onclick: =/c = { display: negate(/c/display), waiting: true, expr: "" }, children: ["±"] } }, { button: { class: "btn btn-function", onclick: =/c = { display: toPercent(/c/display), waiting: true, expr: "" }, children: ["%"] } }, // Divison is our first operator. This is where it gets tricky. // When you click `+`, you can't just link `prev` to `display`. // If you did, `prev` would update every time you selected a new digit for the**second**number, // breaking the math. We need a snapshot of the value at that exact moment. // Excel solves this with INDIRECT, effectively dereferencing a cell. JPRX borrows the same concept: { button: { class: "btn btn-operator", onclick: =/c = { prev: indirect(/c/display), // Capture the value right now expr: concat(/c/display, " ÷"), op: "/", waiting: true }, children: ["÷"] } }, // Row 2: 7, 8, 9, × // I have 10 number buttons. Do I write 10 handlers? Do I write a loop? In React or Vue, // you'd probably map over an array. With JPRX, the DOM is the data key and although map is available, // I represent the calculator using literals in this example. In a future article I will cover map. // By giving each button an `id` (e.g., `id: "7"`), we write a uniform logic expression that adapts // to whichever element triggered it. We just reference $this.id in JPRX and use an xpath to get the text // content for the child node, #../@id. In cDOM (not JPRX) '#' delimits the start of an xpath expression { button: { id: "7", class: "btn btn-number", onclick: =/c = { display: if(/c/waiting, $this.id, if(/c/display==0, $this.id, concat(/c/display, $this.id))), waiting: false }, children: [#../@id] // use xpath (starting char #) to get the text for the button from parent id } }, // Here's what is happening: // Waiting for input? (e.g., just hit `+`) → Replace the display with the button's ID. // Displaying "0"? → Replace it (avoids "07"). // Otherwise: → Append the button's ID. // This is replicated identically for every number button. No loops, no external helper functions. { button: { id: "8", class: "btn btn-number", onclick: =/c = { display: if(/c/waiting, $this.id, if(/c/display==0), $this.id, concat(/c/display, $this.id))), waiting: false }, children: [#../@id] } }, { button: { id: "9", class: "btn btn-number", onclick: =/c = { display: if(/c/waiting, $this.id, if(/c/display==0, $this.id, concat(/c/display, $this.id))), waiting: false }, children: [#../@id] } }, { button: { class: "btn btn-operator", onclick: =/c = { prev: indirect(/c/display), expr: concat(/c/display, " ×"), op: "*", waiting: true }, children: ["×"] } }, // Row 3: 4, 5, 6, − { button: { id: "4", class: "btn btn-number", onclick: =/c = { display: if(/c/waiting, $this.id, if(/c/display==0, $this.id, concat(/c/display, $this.id))), waiting: false }, children: [#../@id] } }, { button: { id: "5", class: "btn btn-number", onclick: =/c = { display: if(/c/waiting, $this.id, if(/c/display==0, $this.id, concat(/c/display, $this.id))), waiting: false }, children: [#../@id] } }, { button: { id: "6", class: "btn btn-number", onclick: =/c = { display: if(/c/waiting, $this.id, if(/c/display==0, $this.id, concat(/c/display, $this.id))), waiting: false }, children: [#../@id] } }, { button: { class: "btn btn-operator", onclick: =/c = { prev: indirect(/c/display), expr: concat(/c/display, " −"), op: "-", waiting: true }, children: ["−"] } }, // Row 4: 1, 2, 3, +, use set and eq just to demonstrate equivalence with = and == // the buttons below use 'set' in place of the infix operator '=', just to show a different way of doing things { button: { id: "1", class: "btn btn-number", onclick: =set(/c, { display: if(/c/waiting, $this.id, if(eq(/c/display, "0"), $this.id, concat(/c/display, $this.id))), waiting: false }), children: [#../@id] } }, { button: { id: "2", class: "btn btn-number", onclick: =set(/c, { display: if(/c/waiting, $this.id, if(eq(/c/display, "0"), $this.id, concat(/c/display, $this.id))), waiting: false }), children: [#../@id] } }, { button: { id: "3", class: "btn btn-number", onclick: =set(/c, { display: if(/c/waiting, $this.id, if(eq(/c/display, "0"), $this.id, concat(/c/display, $this.id))), waiting: false }), children: [#../@id] } }, { button: { class: "btn btn-operator", onclick: =set(/c, { prev: indirect(/c/display), expr: concat(/c/display, " +"), op: "+", waiting: true }), children: ["+"] } }, // Row 5: 0, ., = { button: { id: "0", class: "btn btn-number btn-wide", onclick: =set(/c, { display: if(/c/waiting, $this.id, if(eq(/c/display, "0"), "0", concat(/c/display, $this.id))), waiting: false }), children: [#../@id] } }, { button: { class: "btn btn-number", onclick: =set(/c, { display: if(/c/waiting, "0.", if(contains(/c/display, "."), /c/display, concat(/c/display, "."))), waiting: false }), children: ["."] } }, // Finally, the math. We need to say: // 1. Take the snapshot we stored // 2. Apply the current operator // 3. combine it with what's on screen now // This is the job of calc(). If prev == 8 and op == * and display = 5, then calc would be evaluated as calc("8 * 5") // To keep the syntax a little cleaner we also use $(<path>) as a shorthand for indirect. { button: { class: "btn btn-equals", onclick: =set(/c, { display: if(eq(/c/op, ""), /c/display, calc(concat("$('/c/prev') ", /c/op, " $('/c/display')"))), expr: concat(/c/expr, " ", /c/display, " ="), prev: "", op: "", waiting: true }), children: ["="] } } ] } }, // Branding { div: { class: "branding", children: [ { span: { children: [ "Built with ", { a: { href: "https://github.com/anywhichway/lightview", target: "_blank", children: ["Lightview"] } }, " cDOM • No custom JS!" ] } } ] } } ] } } Cargar cDOM a través de Lightview Hypermedia Lightview admite la capacidad de hipermedia similar a Al permitir el uso de la atributos sobre casi cualquier elemento. HTMX src Simplemente referencia a El archivo que utiliza : cDOM src <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="description" content="A beautiful calculator built with Lightview cDOM and JPRX reactive expressions - no custom JavaScript!"> <title>Calculator | Lightview cDOM</title> <link rel="stylesheet" href="calculator.css"> <!-- Load Lightview scripts --> <script src="/lightview.js"></script> <!-- DOM as JSON and reactivity support --> <script src="/lightview-x.js"></script> <!-- hypermedia support --> <script src="/lightview-cdom.js"></script> <-- cDOM/JPRX support --> </head> <body> <!-- The calculator cDOM is loaded via Lightview's hypermedia src attribute --> <div id="app" src="./calculator.cdomc"></div> </body> </html> El Funcionan como un HTML o Tag - Lightview recupera automáticamente el archivo, lo analizará y convertirá el contenido reactivo en el elemento objetivo. src <img> <script> .cdomc ¿Por qué construir de esta manera? Usted puede mirar y preguntando: concat("$('/c/prev') ...") ¿Por qué en el mundo no escribirías parseFloat(prev) + parseFloat(curr) ? Si usted es un código de escritura humano para usted mismo? probablemente lo haría. Lightview soporta manejadores JS estándar por esa razón. Pero si usted está construyendo una infraestructura para Adherirse a un camino declarativo basado en JSON ofrece cosas que el código crudo no puede: AI Agents Sandboxing: Se ejecuta en un entorno controlado. La lógica no puede acceder a la 'finestra', hacer solicitudes de recogida global, o ejecutar código secundario arbitrario. Esto hace que sea seguro "intercambiar caliente" la lógica de la interfaz generada por un LLM en tiempo real. Portabilidad: toda esta interfaz de usuario -lógica y todo- es sólo datos. puede ser enviado desde un servidor, almacenado en una base de datos, o transmitido desde un modelo de IA. Modelo mental: Forza una separación clara entre las transformaciones del estado y la estructura de la visión, que es exactamente cómo los LLMs razonan mejor. Esta calculadora demuestra que "declarativo" no tiene que significar "estúpido".Con los primitivos correctos -estado, condicionales y referenciamiento basado en el camino- puede construir interacciones ricas y complejas sin abandonar nunca la estructura de datos. La imagen más grande Esta serie no se trata sólo de una nueva biblioteca; se trata de encontrar la capa de abstracción adecuada para la era de la IA. en , examinamos los riesgos de dejar que los LLM escriban scripts crudos e introdujeron la filosofía de "Datos como UI". El futuro de la UI generada por IA En este artículo, mostramos que "Datos como interfaz de usuario" no significa "interfaz de usuario estúpida".Trabajamos estado, contexto, snapshots de datos, matemáticas y navegación DOM con "xpath" sin ejecutar una sola línea de JavaScript personalizado. cDOM define la estructura. JPRX define el comportamiento. Es la reactividad sin la compilación y la interfaz de usuario sin los riesgos de seguridad. Pruebe usted mismo La calculadora completa está disponible en: Demo en vivo: https://lightview.dev/docs/calculator.html Código de fuente: https://github.com/anywhichway/lightview/blob/main/docs/calculator.html https://lightview.dev/docs/calculator.html https://github.com/anywhichway/lightview/blob/main/docs/calculator.html