How a JSON or JSON-like language enables the next generation of safe human and AI-generated UIs Įvadas į , Aš atkreipiau dėmesį į tai, kad žaliavos JavaScript, sukurtas AI generuojamo kodo, yra saugumo problema, o JavaScript lankstumas be rėmo gali sukelti sunku valdyti kodą. arba a c) su (JPRX jei), jei norime pasitikėti LLM kurti mūsų sąsajas. AI generuojamos UI ateitis A2 ir Kompiuterinis namas JSON pointeris Reguliarios išraiškos Tai taip pat padeda, jei požiūris yra pagrįstas pramonės standartais, dėl kurių yra daug dokumentų ir pavyzdžių, kuriuos tikriausiai suvartojo LLM mokymo metu. cCOM ir JPRX tai daro; jie įtraukia sąvokas ir sintaksę iš JSON Pointers, JSON Schema ir XPath. Mano ankstesniame straipsnyje, norėdamas parodyti, kaip veikia cDOM ir JPRX, aš naudoju reaktyvųjį skaitiklį, bet būkime realūs: reaktyvūs skaitikliai ir darbų sąrašai yra lengvi. Bet kokia sistema atrodo elegantiška, kai logika tinka ant servetėlės. Norint įrodyti, kad JSON pagrįstas požiūris iš tikrųjų laikosi, jums reikia problemų su netvarka, krašto atvejais ir skirtingais veikimo būdais. Skaičiuoklės iš esmės yra sudėtingos: Įvesties režimai: ar įvedame naują skaičių, ar pridedame prie esamo? Kas atsitinka, kai paspaudžiate „+“, tada „-“ ir tada „*“ be lygių? DRY logika: kaip mes sumažinti kodo skirtumus tarp 10 tvarkytojų 0-9 mygtukams? Taigi, aš paprašiau Claude Opus sukurti visiškai funkcionalią, iOS stiliaus skaičiuoklę, naudojant - tik deklaratyvios cDOM ir JPRX išraiškos. zero custom JavaScript functions Tai, kad AI galėtų gaminti deklaratyviąją skaičiuoklę su mažai raginimu grynai iš dokumentų, parodo dar vieną dalyką, kurį padariau savo ankstesniame straipsnyje: cDOM ir JPRX yra ne tik nauja sintaksė. Kodėl Siekiant sumažinti simbolių ir citatų triukšmą, leidžiant inline paaiškinimą, aš naudoju cDOMC, suspaustą cDOM versiją. Įprastas cDOM nepalaiko komentarų ir reikalauja citatų aplink atributus ir JPRX išraiškas. { 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!" ] } } ] } } ] } } CDOM įkėlimas per „Lightview Hypermedia“ „Lightview“ palaiko hipermedijos funkcijas, panašias į Suteikiant galimybę naudoti Atitinka beveik bet kokį elementą. HTMX src Paprasta nuoroda a Failas naudojamas : 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> Tų Atributas veikia kaip HTML arba žymės - „Lightview“ automatiškai failą, analizuoja jį ir paverčia reaktyvų turinį į tikslinį elementą. src <img> <script> .cdomc Kodėl statyti tokiu būdu? Galite pažvelgti į Ir paklauskite: concat("$('/c/prev') ...") Kodėl pasaulyje jūs ne tik rašyti parseFloat(prev) + parseFloat(curr) ? Jei esate žmogaus kodo rašymas sau? Jūs tikriausiai norėtumėte. „Lightview“ palaiko standartinius JS tvarkykles būtent dėl šios priežasties. Jei norite sukurti infrastruktūrą Laikydamiesi deklaratyvaus, JSON pagrįsto kelio, siūlome dalykus, kurių žaliavinis kodas negali: AI Agents Sandboxing: Tai veikia kontroliuojamoje aplinkoje. logika negali pasiekti "lango", padaryti pasaulinius paieškos užklausas, arba vykdyti bet kokią antrinį kodą. Perkeliamumas: visa ši UI - logika ir viskas - yra tik duomenys.Jis gali būti siunčiamas iš serverio, saugomas duomenų bazėje arba transliuojamas iš AI modelio. Psichikos modelis: Jis verčia aiškiai atskirti valstybines transformacijas ir požiūrio struktūrą, kuri yra būtent tai, kaip LLM geriausiai supranta. Šis skaičiuoklė įrodo, kad "deklaratyvus" nebūtinai reiškia "kvailas". su teisingais primityvais - būsenos, sąlyginiais ir keliu pagrįsta nuoroda - galite kurti turtingas, sudėtingas sąveikas, niekada nepaliekant duomenų struktūros. Didžiausias paveikslas Ši serija yra ne tik apie naują biblioteką, bet ir apie tai, kaip rasti tinkamą abstrakcijos sluoksnį AI amžiui. į , mes pažvelgėme į riziką, leidžiančią LLM rašyti žaliuosius scenarijus, ir pristatėme "duomenų kaip UI" filosofiją. AI generuojamos UI ateitis Šiame straipsnyje parodėme, kad „duomenys kaip UI“ nereiškia „dumb UI.“ Mes tvarkėme būseną, kontekstą, duomenų akimirkas, matematiką ir DOM navigaciją su „xpath“, nevykdydami vienos eilutės pritaikytos JavaScript. cDOM apibrėžia struktūrą. JPRX apibrėžia elgesį. Tai reaktyvumas be kompiliacijos ir naudotojo sąsaja be saugumo rizikos. Išbandykite patys Visą skaičiuoklę galima rasti: Gyvas demonstravimas: https://lightview.dev/docs/calculator.html Šaltinio kodas: 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