diff --git a/.github/workflows/build-swift.yml b/.github/workflows/build-swift.yml new file mode 100644 index 0000000000..4ab495cd4f --- /dev/null +++ b/.github/workflows/build-swift.yml @@ -0,0 +1,80 @@ +name: Build Swift + +on: + push: + branches: [main] + paths: + - "packages/swift-lang/**" + - "packages/lang-core/src/**" + - "packages/react-ui/src/genui-lib/**" + - "packages/react-ui/src/utils/sentinelParser.ts" + - "packages/react-ui/src/components/Charts/utils/styleUtils.ts" + - "packages/react-ui/src/components/Charts/shared/core/PortalTooltip/utils/**" + - ".github/workflows/build-swift.yml" + pull_request: + branches: [main] + paths: + - "packages/swift-lang/**" + - "packages/lang-core/src/**" + - "packages/react-ui/src/genui-lib/**" + - "packages/react-ui/src/utils/sentinelParser.ts" + - "packages/react-ui/src/components/Charts/utils/styleUtils.ts" + - "packages/react-ui/src/components/Charts/shared/core/PortalTooltip/utils/**" + - ".github/workflows/build-swift.yml" + +permissions: + contents: read + +jobs: + swift: + runs-on: macos-15 + defaults: + run: + working-directory: packages/swift-lang + steps: + - uses: actions/checkout@v4 + + - name: Build + run: swift build + + - name: Test + run: swift test + + - name: Lint + run: swift format lint --strict -r Sources Tests Package.swift + + - name: Build for iOS + run: xcodebuild build -scheme OpenUI-Package -destination "generic/platform=iOS Simulator" -quiet + + # The Swift port is checked against fixtures generated from lang-core and the + # chat library. Fail when they no longer match what the TypeScript produces. + fixtures: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v6 + - uses: actions/setup-node@v4 + with: + node-version: v24.21.0 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build lang-core and react-ui + run: pnpm --filter "@openuidev/react-ui..." --workspace-concurrency=1 run build + + - name: Regenerate fixtures + run: node packages/swift-lang/Scripts/generate-fixtures.mjs + env: + OPENUI_TELEMETRY_DISABLED: "1" + + # Status rather than diff, so a newly generated fixture file counts too. + - name: Check fixtures are up to date + run: | + fixtures="packages/swift-lang/Tests/OpenUILangTests/Fixtures packages/swift-lang/Tests/OpenUISwiftUITests/Fixtures" + if [ -n "$(git status --porcelain -- $fixtures)" ]; then + git status --short -- $fixtures + git diff -- $fixtures | head -100 + exit 1 + fi diff --git a/packages/swift-lang/.gitattributes b/packages/swift-lang/.gitattributes new file mode 100644 index 0000000000..f44e762b9b --- /dev/null +++ b/packages/swift-lang/.gitattributes @@ -0,0 +1,5 @@ +# Generated from the TypeScript packages (see Scripts/); collapsed in diffs. +Tests/OpenUILangTests/Fixtures/*.json linguist-generated=true +Tests/OpenUISwiftUITests/Fixtures/*.json linguist-generated=true +Sources/OpenUILang/ChatComponents.swift linguist-generated=true +Sources/OpenUILang/OpenUIComponents.swift linguist-generated=true diff --git a/packages/swift-lang/.gitignore b/packages/swift-lang/.gitignore new file mode 100644 index 0000000000..2d9f16e2d2 --- /dev/null +++ b/packages/swift-lang/.gitignore @@ -0,0 +1,2 @@ +.build/ +.swiftpm/ diff --git a/packages/swift-lang/Package.swift b/packages/swift-lang/Package.swift new file mode 100644 index 0000000000..0001f98c0d --- /dev/null +++ b/packages/swift-lang/Package.swift @@ -0,0 +1,25 @@ +// swift-tools-version:6.0 +import PackageDescription + +let package = Package( + name: "OpenUI", + platforms: [.iOS(.v17), .macOS(.v14)], + products: [ + .library(name: "OpenUILang", targets: ["OpenUILang"]), + .library(name: "OpenUISwiftUI", targets: ["OpenUISwiftUI"]), + ], + targets: [ + .target(name: "OpenUILang"), + .target(name: "OpenUISwiftUI", dependencies: ["OpenUILang"]), + .testTarget( + name: "OpenUILangTests", + dependencies: ["OpenUILang"], + resources: [.copy("Fixtures")] + ), + .testTarget( + name: "OpenUISwiftUITests", + dependencies: ["OpenUISwiftUI"], + resources: [.copy("Fixtures")] + ), + ] +) diff --git a/packages/swift-lang/README.md b/packages/swift-lang/README.md new file mode 100644 index 0000000000..10c230d93c --- /dev/null +++ b/packages/swift-lang/README.md @@ -0,0 +1,281 @@ +# OpenUI for Swift + +Native Swift support for OpenUI Lang. Define model-renderable components in Swift, generate prompts from them, parse streamed OpenUI Lang, and render it with SwiftUI on iOS and macOS. + +Two libraries: + +- **`OpenUILang`**: parser, streaming parser, runtime (state, actions, queries), component library definitions and prompt generation. Plain Swift with Foundation. +- **`OpenUISwiftUI`**: the SwiftUI renderer and native views for every component of the OpenUI chat library. + +**Links:** [OpenUI Lang docs](https://openui.com/docs/openui-lang) | [GitHub repo](https://github.com/thesysdev/openui) + +## Install + +Requires Swift 6 and iOS 17 or macOS 14. + +The package lives in `packages/swift-lang` of this repository. Swift Package Manager only resolves packages at the root of a git repository, so for now add it as a local package (File > Add Package Dependencies > Add Local… in Xcode), or in `Package.swift`: + +```swift +dependencies: [ + .package(path: "../openui/packages/swift-lang"), +], +targets: [ + .target(name: "App", dependencies: [.product(name: "OpenUISwiftUI", package: "swift-lang")]), +] +``` + +`OpenUISwiftUI` re-exports `OpenUILang`. + +## Overview + +OpenUI for Swift follows the same model as the JavaScript packages: + +1. **Define components** the model may call, with typed props. +2. **Generate a system prompt** from the component library. +3. **Render streamed output** with `OpenUIRenderer` as OpenUI Lang arrives. + +The parser and runtime are a port of `@openuidev/lang-core`, checked against it with conformance fixtures (see [Testing Locally](#testing-locally)). The built-in chat library uses the same component names, arguments and prompt options as `openuiChatLibrary` from `@openuidev/react-ui`, so one backend prompt, or OpenUI Cloud, can drive web and native clients. + +## Quick Start + +### 1. Define a component + +```swift +import OpenUISwiftUI +import SwiftUI + +let greetingSchema = ComponentSchema( + "Greeting", description: "Greets someone by name", + props: [ + Prop("name", .string), + Prop("mood", .enumeration(["happy", "excited"]), .optional), + ]) + +let greeting = SwiftUIComponent(greetingSchema) { props in + Text("Hello, \(props.text("name"))\(props.string("mood") == "excited" ? "!" : ".")") + .font(.title2) +} +``` + +`props` holds the evaluated arguments, mapped from positional arguments by the order of the schema's props. + +### 2. Create a library + +```swift +let stackSchema = ComponentSchema( + "Stack", description: "Vertical stack", + props: [Prop("children", .array(.union([.component("Greeting"), .component("Stack")])))]) + +let stack = SwiftUIComponent(stackSchema) { props in + OpenUINodes(props.array("children")) +} + +let library = SwiftUILibrary(components: [stack, greeting], root: "Stack") +``` + +### 3. Generate a system prompt + +```swift +let systemPrompt = library.prompt(PromptOptions(additionalRules: ["Keep greetings short."])) +``` + +The prompt matches what `lang-core` generates for the same library. If prompts are built on a server, `library.toSpec()` returns a `LibrarySpec` you can pass to `generateSystemPrompt({ library })` from `@openuidev/lang-core`. + +With OpenUI Cloud, send the config block instead; Cloud builds the prompt on its side. It's the same block `generateSystemPrompt({ cloud: true, library })` returns: + +```swift +let systemMessage = try library.cloudConfig(PromptOptions(additionalRules: ["Keep greetings short."])) +``` + +### 4. Render streamed output + +```swift +struct MessageView: View { + let text: String + let isStreaming: Bool + + var body: some View { + OpenUIRenderer(response: text, isStreaming: isStreaming, library: library) { event in + // Buttons, follow-ups and forms continue the conversation through here. + print(event.type, event.humanFriendlyMessage, event.formState ?? [:]) + } + } +} +``` + +Pass the full response text so far on every chunk; the renderer only re-parses what changed. OpenUI Cloud responses wrap the program in `]]>openui:content` and `]]>openui:end` markers; pass them through as they are, like the JavaScript renderers do. + +A finished response renders fully on its first frame, so a restored transcript doesn't flash empty rows. Lay a transcript out in a `VStack` inside the `ScrollView`, not a `LazyVStack`: a lazy stack re-measures rows as it goes, and when that happens inside an animation (an animated `scrollTo`, the keyboard closing) SwiftUI can block the main thread for seconds while response heights settle. + +## The chat library + +`OpenUIChatLibrary.library` renders every `openuiChatLibrary` component natively: cards and text, Markdown, callouts, code, images and galleries, Swift Charts (bar, line, area, horizontal bar, pie, radial, single stacked bar, scatter) and a radar chart, tables and the editable table, forms (inputs, text areas, selects, date pickers, sliders, check boxes, radios, switches, chips, option cards) with validation, buttons and icon buttons, lists and follow-ups, steps, tabs, accordions, sections, carousels, tags, entity lists and the card blocks. + +```swift +OpenUIRenderer(response: text, isStreaming: isStreaming, library: OpenUIChatLibrary.library) { event in + send(event.humanFriendlyMessage) +} + +// The prompt a react-ui app sends with openuiChatPromptOptions: +let prompt = OpenUIChatLibrary.library.prompt(ChatComponents.promptOptions) +// Or for OpenUI Cloud: +let systemMessage = try OpenUIChatLibrary.library.cloudConfig(ChatComponents.promptOptions) +``` + +The views follow react-ui's layouts and behavior rather than restyling them: card blocks use the same responsive grid (one column on phones), tabs and sections follow the stream until the user picks one, charts use react-ui's palette, curves and number formats, with its legends (tap a key to hide that series) and tooltips (hover with a pointer, or tap), and `[n]` citations resolve against the card's sources. Lucide icon names map to SF Symbols with the same category fallbacks. Spacing, fills and corner radii come from `OpenUITheme`, which follows react-ui's design tokens; override it with `.environment(\.openUITheme, theme)`. Its `accent` and `onAccent` color primary buttons, selections and steps (the renderer also tints controls with `accent`), and `chartPalette` replaces the default chart ramp, like react-ui's `defaultChartPalette`, with a palette per chart (`barChartPalette` and the rest) taking precedence. `info`, `success`, `alert` and `danger` color callouts, tags, trends and form errors. + +The schemas in `ChatComponents` live in `OpenUILang`, without SwiftUI, so a Swift server can build the chat prompt too. They're generated from `openuiChatLibrary` (see [Testing Locally](#testing-locally)). + +### The general library + +`OpenUILibrary.library` is react-ui's general `openuiLibrary`, for apps that aren't a chat: the same views without the chat-only follow-ups and sections, plus `Stack` (the root, a flex row or column), `Modal` (a sheet its `$boolean` binding opens; closing it writes `false` back) and a `Card` that takes Stack's layout props. Its schemas and prompt options are `OpenUIComponents`. + +```swift +OpenUIRenderer(response: text, isStreaming: isStreaming, library: OpenUILibrary.library) + +let prompt = OpenUILibrary.library.prompt(OpenUIComponents.promptOptions) +``` + +## API Reference + +### Component Definition + +| API | Description | +| ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `ComponentSchema(name, description:, props:)` | A component's contract with the model: props in positional order. | +| `Prop(name, type, presence, binding:, description:)` | A prop. `presence` is `.required` (default), `.optional` or `.defaulted(value)`. `binding: true` marks a two-way `$state` binding, shown as `$binding` in the prompt. | +| `PropType` | `.string`, `.number`, `.boolean`, `.any`, `.enumeration`, `.literal`, `.array(_, minItems:)`, `.object([Prop])`, `.record`, `.component(name)`, `.union`, `.named(name, type)`, `.actionExpression` | +| `SwiftUIComponent(schema) { props in … }` | Pairs a schema with the SwiftUI view that renders it. | +| `SwiftUILibrary(components:, root:, componentGroups:)` | A library (`Library`). | + +Schemas serialize to the same JSON Schema `lang-core` libraries produce: `schema.jsonSchema`, `library.toJSONSchema()`. + +### Rendering + +| API | Description | +| ------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | +| `OpenUIRenderer(response:isStreaming:library:initialState:toolProvider:publishObservability:queryLoader:onAction:onStateUpdate:onParseResult:onError:)` | Renders a response. `queryLoader` replaces the small spinner shown while queries load. | +| `OpenUINode(value)` / `OpenUINodes(values)` | Render child props (elements, arrays, text) inside a component. | +| `ComponentProps` | `string`, `text`, `number`, `bool`, `array`, `elements`, `children` accessors that tolerate partial values while streaming. | + +### Errors + +`onError` receives the same structured errors as react-lang's `onError`, for an automated correction loop: parser validation errors with fix hints (available components, the expected signature), `parse-failed` when a finished response has no root, and failed `Query`/`Mutation` calls. Errors are reported once the response finishes streaming, and `onError([])` is called when they clear or the next response starts. `OpenUIError.jsonRepresentation` gives the same JSON shape as lang-core's `OpenUIError`. + +### Observability + +`Observability.shared` ports `@openuidev/observability`: a bus that sinks (logging, analytics, a debugging view) listen to with `listen(_:_:)` or `listenAll(_:)`. As each response streams and settles, the runtime emits react-lang's `react-lang:stream` events with the same fields (a stable `id`, `phase`, `updateIndex`, the response, parser metadata, errors and timings), so a sink built for the web reads them too. A settled response with errors is an `error`-level event. Pass `publishObservability: false` to the renderer to stop them. + +```swift +Observability.shared.listen([.error]) { event in + logger.error("\(event.detail["message"]?.stringValue ?? "")") +} +``` + +### Context Helpers + +Components read the renderer state from the environment: + +```swift +@Environment(OpenUIContext.self) private var context +@Environment(\.openUIFormName) private var form +@Environment(FormValidation.self) private var validation: FormValidation? +``` + +| API | Description | +| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | +| `context.triggerAction(label, form:, action:)` | Runs an action: an `Action([...])` plan, a legacy action object, or (with no action) continues the conversation with `label`. | +| `context.stateField(name:binding:form:)` | The field a component reads and writes: a `$state` binding when the prop is bound, otherwise the form field. | +| `context.isStreaming`, `context.isQueryLoading` | Streaming and query status. | + +### Form Validation + +`Form` provides a `FormValidation` to its fields. Fields register `rules` (`{ required: true, minLength: 3, email: true, … }`), primary buttons validate the form before firing their action, and `FormControl` shows the error. + +### Parser, prompt and runtime (`OpenUILang`) + +| API | Description | +| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | +| `Parser(schema:rootName:)` | Batch parser from a library JSON Schema. `parse(_:)`, `makeStreamParser()`. | +| `StreamParser` | `push(_:)` a chunk or `set(_:)` the full text so far; returns the latest `ParseResult`. | +| `OpenUIRuntime` | Streaming parse, store, prop evaluation, form fields, actions and error reporting, without UI (react-lang's `useOpenUIState`). | +| `generatePrompt(_:)`, `library.prompt(_:)` | Build the system prompt. | +| `generateCloudConfig(library:promptOptions:instructions:)`, `library.cloudConfig(_:instructions:)` | OpenUI Cloud's config block (`generateSystemPrompt({ cloud: true })`). | +| `mergeStatements(_:_:rootId:)` | Edit mode: applies a patch program to an existing one. | +| `evaluate(_:_:)`, `Store`, `QueryManager` | Lower-level runtime pieces. | +| `separateContentAndContext(_:)`, `wrapContent(_:)`, `wrapContext(_:)`, `hasLangSyntax(_:)` | OpenUI Cloud's message format, from react-ui's `sentinelParser`: split a stored message into the program and its state. | +| `messageWithState(content:contentHeader:state:)`, `continueConversationMessage(_:)` | Build the messages react-ui sends back: a response with its form state, and a follow-up from an action. | +| `initialState(fromContext:)`, `parseArtifactSentinel(_:)` | Restore form state from a stored message, and read an `]]>openui:artifact` header. | + +## Tool Provider Support (Queries & Mutations) + +`Query(...)` and `Mutation(...)` statements call tools through a `ToolProvider`: + +```swift +let tools = FunctionToolProvider([ + "list_tickets": { args in + let status = args["status"]?.stringValue ?? "open" + return ["rows": [["id": "T-1", "status": .string(status)]]] + } +]) + +OpenUIRenderer(response: text, library: library, toolProvider: tools) +``` + +For an MCP server, `McpToolProvider` plays the part of passing an MCP client to react-lang's Renderer: give it a closure that calls the tool and returns the raw result (`{ content, structuredContent, isError }`) as an `OpenUIValue`, and it unwraps it with `extractToolResult(_:)`. Error results reach `onError` as `mcp-error`. It takes a closure so any MCP client works without this package depending on one: + +```swift +let tools = McpToolProvider { name, arguments in + // e.g. a JSON-RPC tools/call to your MCP server, then JSON.parse(result) + try await mcp.callTool(name, arguments) +} +``` + +Conform your own type to `ToolProvider` for other backends. + +## JSON Schema Output + +`library.toJSONSchema()` produces the same document as `library.toJSONSchema()` in `lang-core` (component schemas under `$defs`, props in positional order), and the parser compiles positional parameters from it. When reading a schema from JSON, use `JSON.parse(_:)`, which keeps object key order. + +## Differences from the JavaScript packages + +- A component inside `@Each` or a ternary that binds a reactive prop to `$state` writes the typed value. In `lang-core` that binding is re-evaluated and nested, so typing writes `undefined`. +- Composite values (arrays, objects) have no identity in Swift, so `==` between two of them is always false. In JavaScript it's true only for the same instance. +- Prop evaluation and SwiftUI views can't throw, so `onError` never reports `runtime-error` or `render-error`. +- `$$` math in text content shows as source; there's no native TeX renderer. +- Code is highlighted with Prism's `vscDarkPlus` and `oneLight` colors by a small built-in tokenizer covering common languages (C-family, JSON, markup, Python, Ruby, shell, YAML, SQL), not by Prism's grammars. +- Touch has no hover, so a chart's tooltip opens on a tap and stays until the same spot is tapped again (or another chart's opens), where react-ui shows it while a finger moves. Hover states need a pointer: the Mac, or an iPad with a trackpad. +- Card blocks laid out as a carousel show the Carousel's step buttons while a pointer is over them. A mouse wheel only scrolls up and down, and there's no shift-scrolling or scrollbar to fall back on like in a browser, so react-ui's button-less card carousels can't be scrolled with a mouse otherwise. +- Editable table cells are always text fields, which suits touch, rather than react-ui's select-then-edit cells. With a keyboard, Up and Down move between rows and Enter moves down; Escape doesn't undo an edit. +- Crowded chart axes keep thinning their labels until the longest fits, down to three labels. react-ui stops thinning once each label has 40px and truncates the rest. +- A visual card whose text block came out empty (say, a loop variable used outside its `@Each`) shows just its photo, where react-ui draws an empty panel over it. +- The image gallery's mosaic has fixed proportions instead of taking its height from the loaded images, so it doesn't jump when they arrive, and the viewer shows each image's `details`. +- Not ported: `jsonToOpenUI`, the deprecated `enrichErrors` (its hints are in `onError`), and the server-side `artifactTool` from `@openuidev/lang-core/cloud`. + +## Testing Locally + +Running the tests needs Xcode (the Command Line Tools alone don't include the testing frameworks). + +```bash +cd packages/swift-lang +swift test +swift format lint --strict -r Sources Tests Package.swift +``` + +The conformance fixtures in `Tests/OpenUILangTests/Fixtures` are generated from the TypeScript packages, which are the source of truth. They cover batch and streaming parsing, prop evaluation, prompts, OpenUI Cloud config, edit-mode merging, error hints, OpenUI Cloud's message format, the chart number formats, and the schemas, prompts and examples of both libraries. After changing `lang-core`, react-ui's libraries, its `sentinelParser` or its chart formatters, rebuild and regenerate them (the script imports react-ui's TypeScript source directly, so it needs Node 22.18 or newer), and regenerate `ChatComponents.swift` and `OpenUIComponents.swift` when either library changed: + +```bash +pnpm run build:packages +node packages/swift-lang/Scripts/generate-fixtures.mjs +node packages/swift-lang/Scripts/generate-chat-components.mjs +``` + +CI regenerates the fixtures from a fresh build and fails if they differ, so drift in `lang-core` or the chat library shows up as a failing check. + +## Documentation + +Full OpenUI Lang documentation is at [openui.com/docs/openui-lang](https://openui.com/docs/openui-lang). + +## License + +MIT diff --git a/packages/swift-lang/Scripts/generate-chat-components.mjs b/packages/swift-lang/Scripts/generate-chat-components.mjs new file mode 100644 index 0000000000..5b551ca5b0 --- /dev/null +++ b/packages/swift-lang/Scripts/generate-chat-components.mjs @@ -0,0 +1,247 @@ +// Regenerates Sources/OpenUILang/ChatComponents.swift and OpenUIComponents.swift +// from openuiChatLibrary and openuiLibrary in @openuidev/react-ui, so the Swift +// libraries can't drift from the web ones. +// +// pnpm run build:packages +// node packages/swift-lang/Scripts/generate-chat-components.mjs +// +// The output is formatted with `swift format`. LibraryComponentsTests checks +// the result against the fixtures from generate-fixtures.mjs. + +import { execFileSync } from "node:child_process"; +import { writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const here = dirname(fileURLToPath(import.meta.url)); +const packages = join(here, "..", ".."); +const sources = join(here, "..", "Sources", "OpenUILang"); + +// Code generation is not product usage, so keep lang-core's telemetry off. +process.env.OPENUI_TELEMETRY_DISABLED = "1"; + +const core = await import(join(packages, "lang-core", "dist", "index.mjs")); +const ui = await import(join(packages, "react-ui", "dist", "genui-lib", "index.mjs")); + +// The library being generated; set by generate() below. +let library; +let known; + +// ── Swift literals ──────────────────────────────────────────────────────── + +function swiftString(text) { + const escaped = [...text] + .map((c) => { + if (c === "\\") return "\\\\"; + if (c === '"') return '\\"'; + if (c === "\n") return "\\n"; + if (c === "\r") return "\\r"; + if (c === "\t") return "\\t"; + const code = c.codePointAt(0); + return code < 0x20 ? `\\u{${code.toString(16)}}` : c; + }) + .join(""); + return `"${escaped}"`; +} + +function swiftValue(value) { + if (value === null) return ".null"; + if (Array.isArray(value)) return `[${value.map(swiftValue).join(", ")}]`; + if (typeof value === "object") { + const entries = Object.entries(value).map(([k, v]) => `${swiftString(k)}: ${swiftValue(v)}`); + return `[${entries.join(", ") || ":"}]`; + } + return typeof value === "string" ? swiftString(value) : JSON.stringify(value); +} + +// ── JSON Schema → PropType ──────────────────────────────────────────────── + +const isFormRules = (s) => + s?.type === "object" && s.properties && "minLength" in s.properties && "pattern" in s.properties; +const isActionExpression = (s) => + Array.isArray(s?.anyOf) && + s.anyOf.length === 3 && + s.anyOf[0].properties?.type?.const === "open_url"; + +function propType(s) { + if (isFormRules(s)) return "formRules"; + if (isActionExpression(s)) return ".actionExpression"; + if (s.$ref) return `.component(${swiftString(s.$ref.split("/").pop())})`; + if (s.anyOf) { + const options = s.anyOf.every((o) => o.$ref) + ? s.anyOf.filter((o) => known.has(o.$ref.split("/").pop())) + : s.anyOf; + return `.union([${options.map(propType).join(", ")}])`; + } + if (s.const !== undefined) return `.literal(${swiftValue(s.const)})`; + switch (s.type) { + case "string": + return s.enum ? `.enumeration([${s.enum.map(swiftString).join(", ")}])` : ".string"; + case "number": + case "integer": + return ".number"; + case "boolean": + return ".boolean"; + case "array": { + const minItems = s.minItems === undefined ? "" : `, minItems: ${s.minItems}`; + return `.array(${propType(s.items)}${minItems})`; + } + case "object": + if (s.propertyNames) return `.record(${propType(s.additionalProperties ?? {})})`; + return `.object([${props(s).join(", ")}])`; + default: + return ".any"; + } +} + +function props(schema, component) { + const required = schema.required ?? []; + const shape = component ? library.components[component].props.shape : {}; + return Object.entries(schema.properties ?? {}).map(([key, s]) => { + const args = [swiftString(key), propType(s)]; + if ("default" in s) args.push(`.defaulted(${swiftValue(s.default)})`); + else if (!required.includes(key)) args.push(".optional"); + if (shape[key] && core.isReactiveSchema(shape[key])) args.push("binding: true"); + if (s.description) args.push(`description: ${swiftString(s.description)}`); + return `Prop(${args.join(", ")})`; + }); +} + +// ── Output ──────────────────────────────────────────────────────────────── + +const lowerFirst = (name) => name[0].toLowerCase() + name.slice(1); +const swiftLines = (items) => items.map((item) => ` ${swiftString(item)},`).join("\n"); + +/// Writes `enumName` with a schema per component, the groups and the prompt +/// options. Components defined exactly as in `shared` (same JSON Schema and +/// bindings) reuse its schemas instead of repeating them. +function generate({ lib, promptOptions, enumName, docs, shared }) { + library = lib; + const defs = lib.toJSONSchema().$defs; + const spec = lib.toSpec(); + const names = Object.keys(spec.components); + known = new Set(names); + const reused = (name) => { + if (!shared?.lib.components[name]) return false; + const other = shared.lib.toJSONSchema().$defs[name]; + const reactive = (l) => + Object.entries(l.components[name].props.shape) + .filter(([, s]) => core.isReactiveSchema(s)) + .map(([key]) => key) + .join(); + return ( + JSON.stringify(other) === JSON.stringify(defs[name]) && reactive(shared.lib) === reactive(lib) + ); + }; + + const schemas = names + .filter((name) => !reused(name)) + .map( + (name) => ` public static let ${lowerFirst(name)} = ComponentSchema( + ${swiftString(name)}, description: ${swiftString(defs[name].description ?? "")}, + props: [${props(defs[name], name).join(", ")}]) +`, + ); + const groups = spec.componentGroups.map((group) => { + const notes = group.notes?.length + ? `, notes: [${group.notes.map(swiftString).join(", ")}]` + : ""; + return ` ComponentGroup(name: ${swiftString(group.name)}, components: [${group.components.map(swiftString).join(", ")}]${notes}),`; + }); + const all = names.map( + (name) => ` ${reused(name) ? `${shared.enumName}.` : ""}${lowerFirst(name)},`, + ); + // A library reusing another's schemas aliases its rules object only when one + // of its own schemas has form rules. + const formRules = shared + ? schemas.some((schema) => /\bformRules\b/.test(schema)) + ? ` /// The form \`rules\` object, for schemas that aren't shared. + static let formRules = ${shared.enumName}.formRules + +` + : "" + : ` /// The structured \`rules\` object shared by form fields. + public static let formRules: PropType = .object( + ["required", "email", "url", "numeric"].map { Prop($0, .boolean, .optional) } + + ["min", "max", "minLength", "maxLength"].map { Prop($0, .number, .optional) } + + [Prop("pattern", .string, .optional)]) + +`; + const output = join(sources, `${enumName}.swift`); + writeFileSync( + output, + `// Generated by Scripts/generate-chat-components.mjs from ${docs.source} in +// @openuidev/react-ui. Don't edit by hand. +// swift-format-ignore-file: LineLength + +${docs.comment} +public enum ${enumName} { +${formRules}${schemas.join("\n")} + /// Prompt groups and notes, as in ${docs.source}. + public static let groups: [ComponentGroup] = [ +${groups.join("\n")} + ] + + /// Every schema, in library order. + public static let all: [ComponentSchema] = [ +${all.join("\n")} + ] + + /// Example responses for the prompt (\`${docs.examples}\`). + public static let examples: [String] = [ +${swiftLines(promptOptions.examples)} + ] + + /// Extra prompt rules for this library (\`${docs.rules}\`). + public static let additionalRules: [String] = [ +${swiftLines(promptOptions.additionalRules)} + ] + + /// The prompt options react-ui pairs with this library (\`${docs.options}\`). + public static let promptOptions = PromptOptions( + additionalRules: additionalRules, examples: examples) +} +`, + ); + execFileSync("swift", ["format", "-i", output]); + console.log( + `wrote ${names.length} ${docs.source} schemas to ${output}` + + (shared ? ` (${names.filter(reused).length} shared with ${shared.enumName})` : ""), + ); +} + +const chat = { + lib: ui.openuiChatLibrary, + promptOptions: ui.openuiChatPromptOptions, + enumName: "ChatComponents", + docs: { + source: "openuiChatLibrary", + examples: "openuiChatExamples", + rules: "openuiChatAdditionalRules", + options: "openuiChatPromptOptions", + comment: `/// Schemas for the OpenUI chat library. Names, prop order, types and +/// descriptions match \`openuiChatLibrary\` in @openuidev/react-ui, so a backend +/// that prompts with either library produces responses both can render. +/// +/// The schemas live in \`OpenUILang\`, without SwiftUI, so a server can build +/// the chat prompt or Cloud config for them too.`, + }, +}; +generate(chat); +generate({ + lib: ui.openuiLibrary, + promptOptions: ui.openuiPromptOptions, + enumName: "OpenUIComponents", + shared: chat, + docs: { + source: "openuiLibrary", + examples: "openuiExamples", + rules: "openuiAdditionalRules", + options: "openuiPromptOptions", + comment: `/// Schemas for react-ui's general OpenUI library, \`openuiLibrary\`: the chat +/// library's components without the chat-only follow-ups and sections, plus +/// \`Stack\` (the root) and \`Modal\`, with \`Card\` taking Stack's layout props. +/// Components both libraries define the same way are \`ChatComponents\`' +/// schemas.`, + }, +}); diff --git a/packages/swift-lang/Scripts/generate-fixtures.mjs b/packages/swift-lang/Scripts/generate-fixtures.mjs new file mode 100644 index 0000000000..287ea8b546 --- /dev/null +++ b/packages/swift-lang/Scripts/generate-fixtures.mjs @@ -0,0 +1,817 @@ +// Regenerates the conformance fixtures in Tests/OpenUILangTests/Fixtures from +// the TypeScript implementation, which is the source of truth. +// +// pnpm run build:packages +// node packages/swift-lang/Scripts/generate-fixtures.mjs +// +// Each fixture pairs an input with what @openuidev/lang-core produces for it. +// The Swift tests feed the same input to the Swift port and compare. + +import { createHash } from "node:crypto"; +import { mkdirSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const here = dirname(fileURLToPath(import.meta.url)); +const packages = join(here, "..", ".."); +const fixtures = join(here, "..", "Tests", "OpenUILangTests", "Fixtures"); + +// Fixture generation is not product usage, so keep lang-core's telemetry off. +process.env.OPENUI_TELEMETRY_DISABLED = "1"; + +const core = await import(join(packages, "lang-core", "dist", "index.mjs")); +const ui = await import(join(packages, "react-ui", "dist", "genui-lib", "index.mjs")); + +// ── Schemas ─────────────────────────────────────────────────────────────── + +const chatSchema = ui.openuiChatLibrary.toJSONSchema(); +// react-ui's general library: the chat components plus Stack (its root) and Modal. +const openuiSchema = ui.openuiLibrary.toJSONSchema(); + +// Small hand-written schema that exercises validation paths the chat library +// doesn't: defaults, const, integer, nested required keys, enum defaults. +const testSchema = { + $defs: { + Card: { + properties: { + children: { + type: "array", + items: { + anyOf: [ + { $ref: "#/$defs/Text" }, + { $ref: "#/$defs/Button" }, + { $ref: "#/$defs/Chart" }, + ], + }, + }, + title: { type: "string" }, + }, + required: ["children"], + }, + Text: { + properties: { + text: { type: "string" }, + size: { type: "string", enum: ["s", "m", "l"], default: "m" }, + }, + required: ["text"], + }, + Button: { + properties: { + label: { type: "string" }, + action: {}, + variant: { type: "string", enum: ["primary", "secondary"] }, + }, + required: ["label"], + }, + Input: { + properties: { name: { type: "string" }, value: { type: "string" } }, + required: ["name"], + }, + Chart: { + properties: { + labels: { type: "array", items: { type: "string" } }, + values: { type: "array", items: { type: "number" } }, + meta: { + type: "object", + properties: { unit: { type: "string" }, scale: { type: "number", default: 1 } }, + required: ["unit"], + }, + }, + required: ["labels", "values"], + }, + Tagged: { + properties: { + count: { type: "integer" }, + flag: { type: "boolean" }, + kind: { const: "x" }, + tone: { type: "string", default: "neutral" }, + }, + required: ["tone"], + }, + }, +}; + +/** Props bound two-way to $state (lang-core's reactive()), per component. */ +function reactiveProps(library) { + const out = {}; + for (const [name, comp] of Object.entries(library.components)) { + const keys = Object.entries(comp.props.shape ?? {}) + .filter(([, schema]) => core.isReactiveSchema(schema)) + .map(([key]) => key); + if (keys.length) out[name] = keys; + } + return out; +} + +// The test schema has no Zod library behind it, so build the minimal shape +// evaluateElementProps reads: components[name].props.shape[prop]. +const testReactive = { Input: ["value"] }; +const testLibrary = { + components: Object.fromEntries( + Object.entries(testSchema.$defs).map(([name, def]) => [ + name, + { + props: { + shape: Object.fromEntries( + Object.keys(def.properties).map((key) => { + const schema = {}; + if (testReactive[name]?.includes(key)) core.markReactive(schema); + return [key, schema]; + }), + ), + }, + }, + ]), + ), +}; + +// Zod hoists recursive data shapes into $defs (__schema0); only the names +// under `properties` are components. +const hoistedSchema = { + properties: { Tree: {} }, + $defs: { + Tree: { properties: { node: { $ref: "#/$defs/__schema0" } }, required: ["node"] }, + __schema0: { type: "object", properties: { label: { type: "string" } } }, + }, +}; + +const schemas = { + chat: { schema: chatSchema, root: "Card", reactive: reactiveProps(ui.openuiChatLibrary) }, + openui: { schema: openuiSchema, root: "Stack", reactive: reactiveProps(ui.openuiLibrary) }, + test: { schema: testSchema, root: "Card", reactive: testReactive }, + hoisted: { schema: hoistedSchema, root: "Tree", reactive: {} }, +}; +const libraries = { chat: ui.openuiChatLibrary, openui: ui.openuiLibrary, test: testLibrary }; + +// ── Corpus ──────────────────────────────────────────────────────────────── + +const chatExamples = ui.openuiChatPromptOptions.examples.map((text, i) => ({ + name: `chat-example-${i + 1}`, + schema: "chat", + input: text, +})); + +const openuiExamples = ui.openuiPromptOptions.examples.map((text, i) => ({ + name: `openui-example-${i + 1}`, + schema: "openui", + input: text, +})); + +const t = (name, input) => ({ name, schema: "test", input }); + +const edgeCases = [ + t("basic", 'root = Card([t1, b1])\nt1 = Text("Hi")\nb1 = Button("Go")'), + t("missing-required", 'root = Card([t1, b1])\nt1 = Text()\nb1 = Button("Go")'), + t("null-required-with-default", "root = Card([t1])\nt1 = Text(null)"), + t("required-default-fills", 'root = Card([x])\nx = Tagged(1, true, "x", null)'), + t("unknown-component", 'root = Card([Mystery("a"), Text("b")])'), + t("excess-args", 'root = Card([Text("a", "m", "extra", 4)])'), + t("type-mismatch-scalar", "root = Card([Text(42)], 7)"), + t("enum-mismatch", 'root = Card([Text("a", "xl"), Button("b", null, "tertiary")])'), + t("enum-partial-deferred", 'root = Card([Text("a", "x'), + t("integer-boolean-const", 'root = Card([x])\nx = Tagged(1.5, "yes", "y", "loud")'), + t( + "nested-object", + 'root = Card([c1, c2, c3])\nc1 = Chart(["a"], [1], {unit: "kg"})\nc2 = Chart(["a"], [1], {scale: 2})\nc3 = Chart(["a"], [1], {unit: 5, scale: "big"})', + ), + t("array-items-pruned", 'root = Card([Chart(["a", 2, "c"], [1, "x", 3])])'), + t("element-in-data-slot", 'root = Card([Chart([Text("a")], [1])])'), + t("unresolved-and-orphaned", 'root = Card([a, missing])\na = Text("x")\nlonely = Text("y")'), + t("cycle", "root = Card([a])\na = Card([b])\nb = Card([a])"), + t("self-reference", "root = Card([root])"), + t("no-root-first-component", 'x = 1\nmain = Card([Text("a")])\nother = Card([])'), + t("root-name-statement", 'Card = Card([Text("named")])\nother = Card([])'), + t("duplicate-ids", 'root = Card([a])\na = Text("first")\na = Text("second")'), + t("only-prose", "Here is your UI, it will appear below."), + t("empty", ""), + t("whitespace", " \n\t \n"), + t( + "fences-and-prose", + 'Sure! Here it is:\n```openui\nroot = Card([a])\na = Text("fenced")\n```\nAnything else?', + ), + t("multiple-fences", '```\nroot = Card([a])\n```\nand\n```\na = Text("second block")\n```'), + t("unclosed-fence", '```openui\nroot = Card([a])\na = Text("str'), + t("fence-in-string", 'root = Card([a])\na = Text("use ``` to fence")'), + t( + "comments", + 'root = Card([a]) // trailing\n# full line\na = Text("not // a comment # here")\n// done', + ), + t("crlf", 'root = Card([a])\r\na = Text("windows")\r\n'), + t( + "string-escapes", + 'root = Card([a, b, c, d])\na = Text("tab\\tnew\\nquote\\" back\\\\ uni\\u00e9 pair\\ud83d\\ude00")\nb = Text(\'single \\\' quote \\n esc \\q\')\nc = Text("bad \\x escape")\nd = Text("emoji 😀 and é and 漢字")', + ), + t("emoji-outside-strings", 'root = Card([a]) 😀\na = Text("ok") ✅'), + t("unterminated-string", 'root = Card([a])\na = Text("streaming tex'), + t("unterminated-escape", 'root = Card([a])\na = Text("ends with \\'), + t( + "numbers", + 'root = Card([Chart(["a","b","c","d","e","f"], [-3, 1.5, 1e3, 007, -0, 2.5e-3])])\n$x = 1e\n$y = 1E+2\n$z = 3.', + ), + t( + "operators", + '$a = 1\n$b = "2"\nroot = Card([Text($a + $b * 2 - -1 > 3 && !$c || $a == "1" ? "yes" : "no")])', + ), + t( + "multiline-ternary", + '$on = true\nroot = Card([t])\nt = Text($on\n ? "enabled"\n : "disabled")', + ), + t( + "member-index", + '$data = {rows: [{name: "a"}, {name: "b"}], "key x": 1}\nroot = Card([Text($data.rows[1].name), Text($data.rows.name), Text($data["key x"])])', + ), + t( + "builtins", + '$items = [3, 1, 2]\nroot = Card([Text("" + @Count($items)), Text("" + @Sum($items)), Text(@Round(2.555, 2) + "")])\nbare = Count($items)', + ), + t( + "actions", + '$n = 0\nroot = Card([b1, b2, b3])\nb1 = Button("Inc", Action([@Set($n, $n + 1), @ToAssistant("Bumped", "ctx")]))\nb2 = Button("Reset", Action([@Reset($n)]))\nb3 = Button("Docs", Action([@OpenUrl("https://openui.com")]))', + ), + t( + "each", + '$todos = [{title: "a", done: false}, {title: "b", done: true}]\nroot = Card(@Each($todos, todo, Text(todo.title)))', + ), + t( + "query-mutation", + '$q = "x"\nusers = Query("list_users", {search: $q, page: $page}, {rows: []}, 30)\nsave = Mutation("save_user", {name: $q})\nroot = Card([Text(users.rows.length + " users"), Button("Save", Action([@Run(save), @Run(users)]))])', + ), + t("state-query", '$data = Query("get", {})\nroot = Card([Text("x")])'), + t("inline-query", 'root = Card([Text(Query("get", {}))])'), + t("assignment", '$v = ""\nroot = Card([Input("name", $v = $value)])'), + t( + "object-key-order", + '$o = {b: 1, 2: "two", a: 3, 1: "one", "10": 10}\nroot = Card([Text("x")])', + ), + t("missing-commas", 'root = Card([Text("a") Text("b"), Text("c"),])'), + t("lowercase-builtin-name", "root = Card([Text(@first([1]))])"), + t("unknown-tokens", 'root = Card([Text("a")]) ; ~ ^\n$ = 3\n= 4\nfoo'), + t( + "combining-characters", + 'root = Card([Text("e\\u0301 cafe\\u0301"), Text("한국어"), Text("ok")])', + ), + t("truncated-mid-call", 'root = Card([a, b])\na = Text("one")\nb = Button("tw'), + t("truncated-mid-object", 'root = Card([Chart(["a"], [1], {unit: "k'), + t( + "data-in-component-slot", + 'root = Card([Text("hi"), { text: "a" }, "b", 3, [Text("x")], true])', + ), + t("data-in-component-slot-via-ref", 'root = Card(items)\nitems = [{ text: "a" }]'), + { name: "hoisted-data-def", schema: "hoisted", input: 'root = Tree({ label: "a" })' }, + // OpenUI Cloud's wire format: a content marker, a fenced program, an end marker. + { + name: "cloud-envelope", + schema: "chat", + input: + ']]>openui:content?thesys=true\n```openui-lang\nroot = Card([h, t])\nh = CardHeader("Q3", "Revenue")\nt = TextContent("Up 12%")\n```\n]]>openui:end', + }, +]; + +// ── Fixture generation ──────────────────────────────────────────────────── + +function parseWith(schemaName, input) { + const { schema, root } = schemas[schemaName]; + return core.createParser(schema, root).parse(input); +} + +/** UTF-16 offsets that don't split a surrogate pair. */ +function safeOffsets(text, step) { + const offsets = []; + for (let i = step; i < text.length; i += step) { + let at = i; + const code = text.charCodeAt(at - 1); + if (code >= 0xd800 && code <= 0xdbff) at += 1; + offsets.push(at); + } + offsets.push(text.length); + return [...new Set(offsets)]; +} + +function streamCheckpoints(schemaName, input) { + const { schema, root } = schemas[schemaName]; + const parser = core.createStreamingParser(schema, root); + const step = Math.max(1, Math.ceil(input.length / 12)); + let last = 0; + return safeOffsets(input, step).map((at) => { + const result = parser.push(input.slice(last, at)); + last = at; + return { at, expected: result }; + }); +} + +const parserCases = [...chatExamples, ...openuiExamples, ...edgeCases].map((c) => ({ + ...c, + expected: parseWith(c.schema, c.input), +})); + +const streamingCases = [...chatExamples, ...openuiExamples, ...edgeCases] + .filter((c) => c.input.length > 0) + .map((c) => ({ ...c, checkpoints: streamCheckpoints(c.schema, c.input) })); + +// `set()` with appended text, then replaced text (which resets the parser). +const setCases = edgeCases.slice(0, 6).map((c) => { + const { schema, root } = schemas[c.schema]; + const parser = core.createStreamingParser(schema, root); + const half = c.input.slice(0, Math.floor(c.input.length / 2)); + const steps = [half, c.input, 'root = Card([Text("replaced")])'].map((text) => ({ + text, + expected: parser.set(text), + })); + return { name: c.name, schema: c.schema, steps }; +}); + +// ── Evaluation ────────────────────────────────────────────────────────── + +const unwrap = (v) => + v && typeof v === "object" && !Array.isArray(v) && "value" in v ? v.value : v; + +/** Parses, seeds a store like the renderer does, and evaluates the tree. */ +function evaluateCase(c) { + const result = parseWith(c.schema, c.input); + const store = core.createStore(); + store.initialize(result.stateDeclarations, {}); + for (const [key, value] of Object.entries(c.state ?? {})) store.set(key, value); + const ctx = { + getState: (name) => unwrap(store.get(name)), + resolveRef: (name) => (c.queryResults ?? {})[name] ?? null, + }; + const errors = []; + const root = result.root + ? core.evaluateElementProps(result.root, { ctx, library: libraries[c.schema], store, errors }) + : null; + return { root, errors }; +} + +const e = (name, input, extra = {}) => ({ name, schema: "test", input, ...extra }); + +const evalCases = [ + ...chatExamples, + ...openuiExamples, + e("operators", edgeCases.find((c) => c.name === "operators").input), + e("member-index", edgeCases.find((c) => c.name === "member-index").input), + e("actions", edgeCases.find((c) => c.name === "actions").input), + e("each", edgeCases.find((c) => c.name === "each").input), + e( + "builtins-all", + '$n = [5, "3", 9, "x", true]\n$rows = [{n: "b", v: 2}, {n: "a", v: 10}, {n: "c", v: "1"}]\nroot = Card([Text("" + @Count($n) + "|" + @Sum($n) + "|" + @Avg($n) + "|" + @Min($n) + "|" + @Max($n) + "|" + @First($n) + "|" + @Last($n) + "|" + @Round(-2.5) + "|" + @Round(1.005, 2) + "|" + @Abs(-3) + "|" + @Floor(1.7) + "|" + @Ceil(1.2)), Text(@Sort($rows, "v", "desc").n + ""), Text(@Sort($rows, "n").n + ""), Text(@Filter($rows, "v", ">", 1).n + ""), Text(@Filter($rows, "n", "contains", "a").n + ""), Text(@Filter($rows, "v", "==", "2").n + "")])', + ), + e( + "arithmetic-edges", + '$z = 0\nroot = Card([Text("" + (5 / $z) + "|" + (-7 % 3) + "|" + (7 % $z) + "|" + ("a" + null) + "|" + (null + 1) + "|" + (true + 1) + "|" + ("5" * "2") + "|" + ("x" - 1) + "|" + (0.1 + 0.2) + "|" + (1 / 3))])', + ), + e( + "truthiness-and-equality", + '$e = ""\n$z = 0\n$arr = []\nroot = Card([Text("" + ($e || "empty") + "|" + ($z && "no") + "|" + ($arr ? "arr-truthy" : "arr-falsy") + "|" + (1 == "1") + "|" + (null == 0) + "|" + ($missing == null) + "|" + !$e)])', + ), + e( + "ternary-components", + '$on = true\nroot = Card([$on ? Text("on", "l") : Button("off"), $on ? null : Text("hidden")])', + ), + e("ternary-components-off", '$on = true\nroot = Card([$on ? Text("on", "l") : Button("off")])', { + state: { $on: false }, + }), + e("state-override", '$count = 1\nroot = Card([Text("count " + $count)])', { + state: { $count: 41 }, + }), + e("reactive-binding", '$v = "hi"\nroot = Card([Input("name", $v), Input("plain", "x")])'), + e("reactive-assign", '$v = ""\nroot = Card([Input("name", $v = $value)])'), + e("query-results", edgeCases.find((c) => c.name === "query-mutation").input, { + queryResults: { users: { rows: [{ id: 1 }, { id: 2 }] }, save: { status: "idle" } }, + }), + e( + "each-actions", + '$items = [{id: 1, t: "a"}, {id: 2, t: "b"}]\n$sel = 0\nroot = Card(@Each($items, it, Button(it.t, Action([@Set($sel, it.id), @ToAssistant("Pick " + it.t)]))))', + ), +]; + +const evalFixtures = evalCases.map((c) => ({ ...c, expected: evaluateCase(c) })); + +// ── Prompts ─────────────────────────────────────────────────────────────── + +const chatSpec = ui.openuiChatLibrary.toSpec(); +const openuiSpec = ui.openuiLibrary.toSpec(); +delete chatSpec.schema; // already in schemas.json + +const tools = [ + "search_docs", + { + name: "list_tickets", + description: "Open support tickets", + inputSchema: { + type: "object", + properties: { + status: { type: "string", enum: ["open", "closed"] }, + limit: { type: "number" }, + }, + required: ["status"], + }, + outputSchema: { + type: "object", + properties: { + rows: { type: "array", items: { type: "object", properties: { id: { type: "string" } } } }, + total: { type: "integer" }, + }, + }, + }, + { name: "ping", inputSchema: {}, outputSchema: undefined }, +]; + +const smallSpec = { + components: { + Box: { signature: "Box(children: (Text | Box)[])", description: "A box" }, + Text: { signature: 'Text(value: string, tone?: "a" | "b")' }, + }, +}; + +// A slice of the chat library that still exercises ActionExpression, +// $binding, groups and ungrouped components, to keep variant fixtures small. +const sliceNames = [ + "Card", + "TextContent", + "Button", + "Buttons", + "Form", + "FormControl", + "Input", + "Select", + "SelectItem", + "Table", + "Col", +]; +const sliceSpec = { + root: chatSpec.root, + components: Object.fromEntries(sliceNames.map((n) => [n, chatSpec.components[n]])), + componentGroups: [ + { + name: "Forms", + components: ["Form", "FormControl", "Input", "Select", "SelectItem"], + notes: ["- Forms note"], + }, + { name: "Buttons", components: ["Button", "Buttons", "Missing"] }, + ], +}; + +const promptCases = [ + { name: "chat-default", spec: chatSpec }, + { name: "chat-options", spec: { ...chatSpec, ...ui.openuiChatPromptOptions } }, + { name: "openui-default", spec: openuiSpec }, + { name: "openui-options", spec: { ...openuiSpec, ...ui.openuiPromptOptions } }, + { name: "slice-bindings", spec: { ...sliceSpec, bindings: true } }, + { + name: "slice-tools", + spec: { + ...sliceSpec, + tools, + toolExamples: ['x = Query("list_tickets", {status: "open"}, {rows: []})'], + }, + }, + { name: "slice-tools-no-bindings", spec: { ...sliceSpec, tools, bindings: false } }, + { + name: "slice-modes", + spec: { + ...sliceSpec, + editMode: true, + inlineMode: true, + preamble: "Custom preamble.", + additionalRules: ["Be brief"], + examples: ["a = 1"], + }, + }, + { name: "slice-no-groups", spec: { ...sliceSpec, componentGroups: undefined } }, + { name: "small-no-root", spec: smallSpec }, + { name: "small-tools-only", spec: { ...smallSpec, toolCalls: true, bindings: false } }, +].map((c) => ({ ...c, expected: core.generatePrompt(c.spec) })); + +const chatComponentSpecs = chatSpec; + +// ── Edit mode ───────────────────────────────────────────────────────────── + +const base = [ + 'root = Card([title, body, $filter], "Report")', + 'title = TextContent("Q3", "large")', + "body = Table([colA, colB])", + 'colA = Col("Region", ["EU", "US"])', + 'colB = Col("Revenue", [1, 2])', + '$filter = "all"', +].join("\n"); + +const mergeCases = [ + { name: "empty-existing", existing: "", patch: 'root = Card([a])\na = TextContent("x")' }, + { name: "empty-patch", existing: base, patch: " \n" }, + { name: "replace", existing: base, patch: 'title = TextContent("Q4", "large")' }, + { + name: "add-referenced", + existing: base, + patch: 'root = Card([title, body, note])\nnote = Callout("info", "Hi")', + }, + { name: "add-unreferenced", existing: base, patch: 'orphan = TextContent("never shown")' }, + { name: "delete", existing: base, patch: "body = null\nroot = Card([title])" }, + { name: "gc-after-replace", existing: base, patch: 'body = TextContent("no table")' }, + { name: "keeps-unused-state", existing: base, patch: "root = Card([title])\n$extra = 1" }, + { name: "root-deleted", existing: base, patch: "root = null" }, + { name: "fenced-patch", existing: base, patch: '```openui\ntitle = TextContent("Fenced")\n```' }, + { + name: "multiline-and-strings", + existing: base, + patch: + 'body = Table([\n colA,\n colB\n])\ntitle = TextContent("a ) ] } \\" \' \\n", "large")', + }, + { name: "single-quotes", existing: base, patch: "title = TextContent('it\\'s (fine')" }, + { + name: "crlf", + existing: base.replaceAll("\n", "\r\n"), + patch: 'title = TextContent("CRLF")\r\n', + }, + { + name: "duplicate-existing", + existing: `${base}\ntitle = TextContent("dup")`, + patch: 'colA = Col("Area", [])', + }, + { name: "delete-then-add", existing: base, patch: 'title = null\ntitle = TextContent("back")' }, + { + name: "query-refs", + existing: + 'root = Card([t])\nt = Table([c])\nc = Col("n", q.rows)\nq = Query("list", {}, {rows: []})', + patch: 'c = Col("name", q.rows)', + }, + { + name: "custom-root", + existing: 'main = Card([a])\na = TextContent("x")\nb = TextContent("y")', + patch: 'a = TextContent("z")', + rootId: "main", + }, + { name: "unclosed-patch", existing: base, patch: 'title = TextContent("cut' }, + { + name: "chat-example", + existing: chatExamples[0].input, + patch: 'header = CardHeader("Edited", "by a patch")', + }, +].map((c) => ({ ...c, expected: core.mergeStatements(c.existing, c.patch, c.rootId) })); + +// ── Error hints ─────────────────────────────────────────────────────────── + +// What react-lang's onError reports for parser errors: lang-core's enrichErrors. +const chatNames = Object.keys(ui.openuiChatLibrary.components); +const errorCases = [ + ["unknown-component", 'root = Card([Mystery("a"), TextContent("b")])'], + ["missing-required", "root = Card([TextContent()])"], + ["null-required", "root = Card([TextContent(null)])"], + ["inline-reserved", 'root = Card([TextContent(Query("get", {}))])'], + ["excess-args", 'root = Card([CardHeader("a", "b", "c", "d")])'], + ["type-mismatch", "root = Card([TextContent(42)])"], + ["several", "root = Card([a, b])\na = Foo()\nb = Button()"], +].map(([name, input]) => ({ + name, + input, + expected: core.enrichErrors(parseWith("chat", input).meta.errors, chatSchema, chatNames), +})); + +// ── Cloud config ────────────────────────────────────────────────────────── + +const cloudLibrary = { + root: "Card", + components: { Card: { signature: "Card(children: Text[])" } }, + componentGroups: [{ name: "Layout", components: ["Card", "Text"], notes: ["- Note"] }], + schema: { + $defs: { + Card: { + properties: { children: { type: "array", items: { $ref: "#/$defs/Text" } } }, + required: ["children"], + }, + Text: { properties: { value: { type: "string" } }, required: ["value"] }, + }, + }, +}; + +const brokenLibrary = { + root: "Missing", + components: {}, + componentGroups: [{ name: "G", components: ["Card", "Nope"] }, { name: 1 }], + schema: { + $defs: { + Card: { + properties: { + children: { type: "array", items: { $ref: "#/$defs/Gone" } }, + x: { $ref: "bad" }, + }, + required: ["children", "absent", 3], + }, + Text: { properties: [] }, + Bad: "nope", + }, + }, +}; + +function cloudCase(name, spec) { + try { + return { name, spec, expected: core.generateSystemPrompt({ cloud: true, ...spec }) }; + } catch (error) { + return { name, spec, error: error.message }; + } +} + +const cloudCases = [ + cloudCase("built-in", {}), + cloudCase("built-in-instructions", { instructions: "Answer in French." }), + cloudCase("built-in-empty-options", { promptOptions: { preamble: "" } }), + cloudCase("built-in-with-options", { promptOptions: { examples: [] } }), + cloudCase("library", { library: cloudLibrary }), + cloudCase("library-options", { + library: cloudLibrary, + promptOptions: { + preamble: "Be brief.", + additionalRules: ["One card"], + examples: ["a = 1"], + tools: ["x"], + editMode: true, + }, + instructions: "Extra.", + }), + cloudCase("library-empty-strings", { + library: cloudLibrary, + promptOptions: { preamble: "" }, + instructions: "", + }), + cloudCase("library-without-root", { + library: { ...cloudLibrary, root: undefined, componentGroups: undefined }, + }), + cloudCase("library-with-id", { library: { ...cloudLibrary, id: "lib-1" } }), + cloudCase("broken-library", { library: brokenLibrary }), + cloudCase("no-defs", { library: { components: {}, schema: { $defs: {} } } }), + cloudCase("no-schema", { library: { root: "", components: {} } }), +]; + +// The chat library's Cloud config is ~80 KB, so only its digest is stored. +const chatCloudConfig = core.generateSystemPrompt({ + cloud: true, + library: ui.openuiChatLibrary.toSpec(), + promptOptions: ui.openuiChatPromptOptions, +}); +const chatCloud = { + length: chatCloudConfig.length, + sha256: createHash("sha256").update(chatCloudConfig).digest("hex"), +}; + +// ── Cloud messages ──────────────────────────────────────────────────────── + +// react-ui doesn't export its sentinel parser, so read the source directly +// (Node strips the TypeScript types). +const sentinel = await import(join(packages, "react-ui", "src", "utils", "sentinelParser.ts")); + +const messageInputs = [ + ["empty", ""], + ["plain text", "Just some text"], + ["content only", "]]>openui:content\nroot = Card([])"], + [ + "content with header attrs", + "]]>openui:content?thesys=true&libraryVersion=0.1.0\nroot = Card([])", + ], + ["content and context", ']]>openui:content\nroot = Card([])\n]]>openui:context\n[{"f":1}]'], + ["context only", 'Hello\n]]>openui:context\n["User clicked: Go"]'], + ["context then content", "]]>openui:context\n[1]\n]]>openui:content\nroot = X()"], + ["last content wins", "]]>openui:content\nold\n]]>openui:content\nnew"], + ["crlf separators", "]]>openui:content\r\nroot = X()\r\n]]>openui:context\r\n[1]"], + ["end marker", "]]>openui:content\nroot = X()\n]]>openui:end"], + ["end marker with attrs", "]]>openui:content\nroot = X()\n]]>openui:end?status=done"], + ["text after end marker", "]]>openui:content\nroot = X()\n]]>openui:end\ntrailing"], + ["end between sections", "]]>openui:content\na\n]]>openui:end\n]]>openui:context\n[1]"], + ["two end markers", "]]>openui:content\na\n]]>openui:end\n]]>openui:end"], + ["crlf before end", "]]>openui:content\na\r\n]]>openui:end"], + ["partial tail one char", "]]>openui:content\nroot = X()\n]"], + ["partial tail sentinel", "]]>openui:content\nroot = X()\n]]>openui:"], + ["partial context marker", "]]>openui:content\nroot = X()\n]]>openui:cont"], + ["partial end marker", "]]>openui:content\nroot = X()\n]]>openui:e"], + ["bracket that isn't a marker", "]]>openui:content\nx = [1, 2]"], + ["marker header without newline", "]]>openui:content"], + ["markers on one line", "]]>openui:content]]>openui:context\n[1]"], + ["emoji", ']]>openui:content\nroot = TextContent("🙂 hi")\n]]>openui:context\n["🎉"]'], + ["legacy xml", 'root = X()\n[{"a":1}]'], + ["legacy content only", "root = X() "], + ["legacy context only", 'text\n["x"]\n'], + ["legacy unclosed", "root = X()"], + ["legacy text after context", "[1] more"], +]; + +const langSyntaxInputs = [ + "```openui-lang\nroot = X()\n```", + "root = X()", + "root=X()", + " root = X()", + "intro\n\troot = X()", + "intro\n\n root\n= X()", + "rooted = 1", + "x root = 1", + "Just text", + "", +]; + +const artifactInputs = [ + ']]>openui:artifact {"artifact_id":"a1","type":"slides","name":"Deck","version":"2"}\nroot = X()', + ']]>openui:artifact {"artifact_id":"a2","type":"presentation"}\nprogram', + ']]>openui:artifact {"artifact_id":"a3","type":"report","name":""}', + ']]>openui:artifact {"artifact_id":"","type":"report"}\nx', + ']]>openui:artifact {"type":"report"}\nx', + ']]>openui:artifact {"artifact_id":"a4","type":"video"}\nx', + ']]>openui:artifact {"artifact_id":"a5","type":"report","version":7}\nx', + "]]>openui:artifact not json\nx", + "]]>openui:artifact []\nx", + ']]>openui:artifact{"artifact_id":"a6","type":"report"}\nx', + "]]>openui:content\nroot = X()", +]; + +const messageCases = { + separate: messageInputs.map(([name, input]) => ({ + name, + input, + expected: sentinel.separateContentAndContext(input), + })), + langSyntax: langSyntaxInputs.map((input) => ({ input, expected: sentinel.hasLangSyntax(input) })), + artifacts: artifactInputs.map((input) => ({ + input, + expected: sentinel.parseArtifactSentinel(input), + })), + wrap: { + content: sentinel.wrapContent("root = X()"), + contentWithHeader: sentinel.wrapContentWithHeader( + "root = X()", + "]]>openui:content?thesys=true", + ), + contentWithoutHeader: sentinel.wrapContentWithHeader("root = X()", undefined), + context: sentinel.wrapContext('[{"a":1}]'), + }, +}; + +// ── Chart number formats ───────────────────────────────────────────────── +// +// react-ui's axis and tooltip formatters, imported from source like +// sentinelParser. toLocaleString follows the machine's locale, so it's pinned +// to en-US here; the Swift tests format with the same locale. + +const charts = join(packages, "react-ui", "src", "components", "Charts"); +const { numberTickFormatter } = await import(join(charts, "utils", "styleUtils.ts")); +const { tooltipNumberFormatter } = await import( + join(charts, "shared", "core", "PortalTooltip", "utils", "index.ts") +); +const toLocaleString = Number.prototype.toLocaleString; +Number.prototype.toLocaleString = function (locales, options) { + return toLocaleString.call(this, locales ?? "en-US", options); +}; +const chartFormatCases = [ + 0, 1, -1, 7, 0.5, 0.125, 1.005, 1.25, 12.345, -0.04, 0.005, 999, 999.99, 1000, 1250, 9999, 10000, + 15500, -2500, 99999.5, 100000, 123456, 999999, 1234567, -7654321, 2.5e9, 1e12, 15e12, 1.23456, +].map((value) => ({ + value, + tick: numberTickFormatter(value), + tooltip: tooltipNumberFormatter(value), +})); +Number.prototype.toLocaleString = toLocaleString; + +// One case per line: still JSON and still diffable case by case, without a +// line for every value. Pretty-printed, the fixtures were most of the +// package's lines. +function fixtureJSON(data) { + const array = Array.isArray(data); + const lines = array + ? data.map((item) => JSON.stringify(item)) + : Object.entries(data).map( + ([key, value]) => `${JSON.stringify(key)}: ${JSON.stringify(value)}`, + ); + return `${array ? "[" : "{"}\n${lines.join(",\n")}\n${array ? "]" : "}"}\n`; +} + +const swiftUIFixtures = join(here, "..", "Tests", "OpenUISwiftUITests", "Fixtures"); + +function write(name, data, folder = fixtures) { + mkdirSync(folder, { recursive: true }); + writeFileSync(join(folder, name), fixtureJSON(data)); +} + +write("schemas.json", Object.fromEntries(Object.entries(schemas))); +write("parser.json", parserCases); +write("streaming.json", streamingCases); +write("stream-set.json", setCases); +write("evaluation.json", evalFixtures); +write("prompts.json", promptCases); +write("merge.json", mergeCases); +write("errors.json", errorCases); +write("cloud.json", { cases: cloudCases, chat: chatCloud }); +write("messages.json", messageCases); +write("chat-spec.json", chatComponentSpecs); +write("openui-spec.json", openuiSpec); +// The libraries' examples, for the SwiftUI renderer's end-to-end render +// tests, and react-ui's chart number formats. +write( + "chat-examples.json", + chatExamples.map(({ name, input }) => ({ name, input })), + swiftUIFixtures, +); +write( + "openui-examples.json", + openuiExamples.map(({ name, input }) => ({ name, input })), + swiftUIFixtures, +); +write("chart-formats.json", chartFormatCases, swiftUIFixtures); + +console.log( + `wrote ${parserCases.length} parser, ${streamingCases.length} streaming, ${setCases.length} set, ${evalFixtures.length} evaluation, ${promptCases.length} prompt, ${mergeCases.length} merge, ${errorCases.length} error, ${cloudCases.length} cloud, ${messageCases.separate.length} message, ${chartFormatCases.length} chart format fixtures`, +); diff --git a/packages/swift-lang/Sources/OpenUILang/AST.swift b/packages/swift-lang/Sources/OpenUILang/AST.swift new file mode 100644 index 0000000000..34a25ccf24 --- /dev/null +++ b/packages/swift-lang/Sources/OpenUILang/AST.swift @@ -0,0 +1,132 @@ +/// Every value that can appear in an OpenUI Lang expression. +/// +/// Mirrors lang-core's `ASTNode` union. Literal and structural nodes are lowered +/// to plain values by the materializer; the reactive and operator nodes survive +/// into props and are resolved by the evaluator at runtime. +public indirect enum ASTNode: Sendable, Equatable { + /// A component or builtin call: `Header("Hello")`, `@Count(items)`. + /// `mappedProps` holds the arguments keyed by prop name for catalog + /// components inside runtime expressions. + case comp(name: String, args: [ASTNode], mappedProps: [ASTEntry]?) + case str(String) + case num(Double) + case bool(Bool) + case null + case arr([ASTNode]) + case obj([ASTEntry]) + /// A reference to another statement. + case ref(String) + /// A placeholder for a reference that could not be resolved. + case ph(String) + /// A reactive state variable, including the `$`: `$count`. + case stateRef(String) + /// A Query or Mutation result, resolved at runtime. + case runtimeRef(String, refType: RuntimeRefType) + case binOp(op: String, left: ASTNode, right: ASTNode) + case unaryOp(op: String, operand: ASTNode) + case ternary(cond: ASTNode, then: ASTNode, else: ASTNode) + case member(obj: ASTNode, field: String) + case index(obj: ASTNode, index: ASTNode) + case assign(target: String, value: ASTNode) + + /// The discriminant lang-core stores in `k`. + public var kind: String { + switch self { + case .comp: return "Comp" + case .str: return "Str" + case .num: return "Num" + case .bool: return "Bool" + case .null: return "Null" + case .arr: return "Arr" + case .obj: return "Obj" + case .ref: return "Ref" + case .ph: return "Ph" + case .stateRef: return "StateRef" + case .runtimeRef: return "RuntimeRef" + case .binOp: return "BinOp" + case .unaryOp: return "UnaryOp" + case .ternary: return "Ternary" + case .member: return "Member" + case .index: return "Index" + case .assign: return "Assign" + } + } + + /// Nodes that must survive parser lowering for runtime evaluation. + public var isRuntimeExpr: Bool { + switch self { + case .stateRef, .runtimeRef, .binOp, .unaryOp, .ternary, .member, .index, .assign: + return true + default: + return false + } + } + + /// Visits this node and every node below it, depth first. + public func walk(_ visit: (ASTNode) -> Void) { + visit(self) + switch self { + case .comp(_, let args, let mappedProps): + for arg in args { arg.walk(visit) } + for entry in mappedProps ?? [] { entry.value.walk(visit) } + case .arr(let elements): + for element in elements { element.walk(visit) } + case .obj(let entries): + for entry in entries { entry.value.walk(visit) } + case .binOp(_, let left, let right): + left.walk(visit) + right.walk(visit) + case .unaryOp(_, let operand): + operand.walk(visit) + case .ternary(let cond, let then, let otherwise): + cond.walk(visit) + then.walk(visit) + otherwise.walk(visit) + case .member(let obj, _): + obj.walk(visit) + case .index(let obj, let index): + obj.walk(visit) + index.walk(visit) + case .assign(_, let value): + value.walk(visit) + default: + break + } + } +} + +public struct ASTEntry: Sendable, Equatable { + public var key: String + public var value: ASTNode + + public init(_ key: String, _ value: ASTNode) { + self.key = key + self.value = value + } +} + +public enum RuntimeRefType: String, Sendable, Equatable { + case query + case mutation +} + +/// A call extracted from a Query or Mutation statement. +public struct CallNode: Sendable, Equatable { + public var callee: String + public var args: [ASTNode] +} + +/// A statement classified at parse time from its identifier token and expression shape. +public enum Statement: Sendable, Equatable { + case value(id: String, expr: ASTNode) + case state(id: String, initial: ASTNode) + case query(id: String, call: CallNode, expr: ASTNode, deps: [String]?) + case mutation(id: String, call: CallNode, expr: ASTNode) + + public var id: String { + switch self { + case .value(let id, _), .state(let id, _), .query(let id, _, _, _), .mutation(let id, _, _): + return id + } + } +} diff --git a/packages/swift-lang/Sources/OpenUILang/Builtins.swift b/packages/swift-lang/Sources/OpenUILang/Builtins.swift new file mode 100644 index 0000000000..96c44e6af9 --- /dev/null +++ b/packages/swift-lang/Sources/OpenUILang/Builtins.swift @@ -0,0 +1,292 @@ +import Foundation + +/// A data builtin callable as `@Name(...)`: its prompt documentation and implementation. +public struct BuiltinDef: Sendable { + public var name: String + /// Signature for prompt docs: `Count(array) → number`. + public var signature: String + public var description: String + public var fn: @Sendable ([OpenUIValue]) -> OpenUIValue +} + +private func arg(_ args: [OpenUIValue], _ index: Int) -> OpenUIValue { + index < args.count ? args[index] : .undefined +} + +/// `String(x ?? "")`. +private func stringOrEmpty(_ value: OpenUIValue) -> String { + value.isNullish ? "" : value.jsString +} + +/// JavaScript property access (`obj[key]`) on plain values. +func jsProperty(_ value: OpenUIValue, _ key: String) -> OpenUIValue { + switch value { + case .object(let object): + return object[key] ?? .undefined + case .array(let items): + if key == "length" { return .number(Double(items.count)) } + if let index = Int(key), String(index) == key, index >= 0, index < items.count { + return items[index] + } + return .undefined + case .string(let string): + let units = Array(string.utf16) + if key == "length" { return .number(Double(units.count)) } + if let index = Int(key), String(index) == key, index >= 0, index < units.count { + return .string(String(decoding: [units[index]], as: UTF16.self)) + } + return .undefined + case .element(let element): + switch key { + case "type": return "element" + case "typeName": return .string(element.typeName) + case "props": return .object(element.props) + case "partial": return .bool(element.partial) + case "statementId": return element.statementId.map { .string($0) } ?? .undefined + default: return .undefined + } + default: + return .undefined + } +} + +/// `String.prototype.includes`, which compares UTF-16 code units (Swift's +/// `contains` would compare grapheme clusters). +func jsIncludes(_ haystack: String, _ needle: String) -> Bool { + let h = Array(haystack.utf16) + let n = Array(needle.utf16) + if n.isEmpty { return true } + guard n.count <= h.count else { return false } + for start in 0...(h.count - n.count) where h[start] == n[0] { + if Array(h[start.. OpenUIValue { + if path.isEmpty || value.isNullish { return .undefined } + if !path.contains(".") { return jsProperty(value, path) } + var current = value + for part in path.split(separator: ".", omittingEmptySubsequences: false) { + if current.isNullish { return .undefined } + current = jsProperty(current, String(part)) + } + return current +} + +/// `Math.min` / `Math.max` semantics: NaN propagates, -0 < +0. +private func jsMin(_ a: Double, _ b: Double) -> Double { + if a.isNaN || b.isNaN { return .nan } + if a == 0 && b == 0 { return a.sign == .minus ? a : b } + return a < b ? a : b +} + +private func jsMax(_ a: Double, _ b: Double) -> Double { + if a.isNaN || b.isNaN { return .nan } + if a == 0 && b == 0 { return a.sign == .plus ? a : b } + return a > b ? a : b +} + +/// `Math.round`: halves round toward +∞. +func jsRound(_ value: Double) -> Double { + if !value.isFinite { return value } + let floored = value.rounded(.down) + let result = value - floored >= 0.5 ? floored + 1 : floored + return result == 0 && value < 0 ? -0.0 : result +} + +/// `String.prototype.localeCompare` with the default locale. +private func jsLocaleCompare(_ a: String, _ b: String) -> Double { + switch a.compare(b, options: [], range: nil, locale: Locale(identifier: "en")) { + case .orderedAscending: return -1 + case .orderedDescending: return 1 + case .orderedSame: return 0 + } +} + +/// `Array.prototype.sort` with a comparator: stable, `undefined` items last, +/// a NaN comparator result counts as equal. +private func jsSort(_ items: [OpenUIValue], _ compare: (OpenUIValue, OpenUIValue) -> Double) + -> [OpenUIValue] +{ + let defined = items.enumerated().filter { $0.element != .undefined } + let undefinedCount = items.count - defined.count + let sorted = defined.sorted { lhs, rhs in + let result = compare(lhs.element, rhs.element) + if result < 0 { return true } + if result > 0 { return false } + return lhs.offset < rhs.offset + } + return sorted.map(\.element) + Array(repeating: .undefined, count: undefinedCount) +} + +private func isNumericForSort(_ value: OpenUIValue) -> Bool { + switch value { + case .number: return true + case .string(let string): return !string.isEmpty && !jsStringToNumber(string).isNaN + default: return false + } +} + +/// Data builtins, keyed by name. Mirrors lang-core's `BUILTINS`. +public let builtins: [String: BuiltinDef] = { + let defs: [BuiltinDef] = [ + BuiltinDef( + name: "Count", signature: "Count(array) → number", description: "Returns array length" + ) { args in + if case .array(let items) = arg(args, 0) { return .number(Double(items.count)) } + return .number(0) + }, + BuiltinDef( + name: "First", signature: "First(array) → element", + description: "Returns first element of array" + ) { args in + guard case .array(let items) = arg(args, 0), let first = items.first, !first.isNullish + else { return .null } + return first + }, + BuiltinDef( + name: "Last", signature: "Last(array) → element", description: "Returns last element of array" + ) { args in + guard case .array(let items) = arg(args, 0), let last = items.last, !last.isNullish + else { return .null } + return last + }, + BuiltinDef( + name: "Sum", signature: "Sum(numbers[]) → number", description: "Sum of numeric array" + ) { args in + guard case .array(let items) = arg(args, 0) else { return .number(0) } + return .number(items.reduce(0) { $0 + toNumber($1) }) + }, + BuiltinDef( + name: "Avg", signature: "Avg(numbers[]) → number", description: "Average of numeric array" + ) { args in + guard case .array(let items) = arg(args, 0), !items.isEmpty else { return .number(0) } + return .number(items.reduce(0) { $0 + toNumber($1) } / Double(items.count)) + }, + BuiltinDef( + name: "Min", signature: "Min(numbers[]) → number", description: "Minimum value in array" + ) { args in + guard case .array(let items) = arg(args, 0), let first = items.first else { + return .number(0) + } + return .number(items.reduce(toNumber(first)) { jsMin($0, toNumber($1)) }) + }, + BuiltinDef( + name: "Max", signature: "Max(numbers[]) → number", description: "Maximum value in array" + ) { args in + guard case .array(let items) = arg(args, 0), let first = items.first else { + return .number(0) + } + return .number(items.reduce(toNumber(first)) { jsMax($0, toNumber($1)) }) + }, + BuiltinDef( + name: "Sort", signature: "Sort(array, field, direction?) → sorted array", + description: "Sort array by field. Direction: \"asc\" (default) or \"desc\"" + ) { args in + guard case .array(let items) = arg(args, 0) else { return arg(args, 0) } + let field = stringOrEmpty(arg(args, 1)) + let descending = (arg(args, 2).isNullish ? "asc" : arg(args, 2).jsString) == "desc" + return .array( + jsSort(items) { a, b in + let av = field.isEmpty ? a : resolveField(a, field) + let bv = field.isEmpty ? b : resolveField(b, field) + if isNumericForSort(av) && isNumericForSort(bv) { + let diff = toNumber(av) - toNumber(bv) + return descending ? -diff : diff + } + let cmp = jsLocaleCompare(stringOrEmpty(av), stringOrEmpty(bv)) + return descending ? -cmp : cmp + }) + }, + BuiltinDef( + name: "Filter", + signature: + "Filter(array, field, operator: \"==\" | \"!=\" | \">\" | \"<\" | \">=\" | \"<=\" | \"contains\", value) → filtered array", + description: "Filter array by field value" + ) { args in + guard case .array(let items) = arg(args, 0) else { return .array([]) } + let field = stringOrEmpty(arg(args, 1)) + let op = arg(args, 2).isNullish ? "==" : arg(args, 2).jsString + let value = arg(args, 3) + return .array( + items.filter { item in + let v = field.isEmpty ? item : resolveField(item, field) + switch op { + case "==": return v.looselyEquals(value) + case "!=": return !v.looselyEquals(value) + case ">": return toNumber(v) > toNumber(value) + case "<": return toNumber(v) < toNumber(value) + case ">=": return toNumber(v) >= toNumber(value) + case "<=": return toNumber(v) <= toNumber(value) + case "contains": return jsIncludes(stringOrEmpty(v), stringOrEmpty(value)) + default: return false + } + }) + }, + BuiltinDef( + name: "Round", signature: "Round(number, decimals?) → number", + description: "Round to N decimal places (default 0)" + ) { args in + let number = toNumber(arg(args, 0)) + let decimals = arg(args, 1).isNullish ? 0 : toNumber(arg(args, 1)) + let factor = pow(10, decimals) + return .number(jsRound(number * factor) / factor) + }, + BuiltinDef(name: "Abs", signature: "Abs(number) → number", description: "Absolute value") { + args in .number(abs(toNumber(arg(args, 0)))) + }, + BuiltinDef( + name: "Floor", signature: "Floor(number) → number", + description: "Round down to nearest integer" + ) { args in .number(toNumber(arg(args, 0)).rounded(.down)) }, + BuiltinDef( + name: "Ceil", signature: "Ceil(number) → number", description: "Round up to nearest integer" + ) { args in .number(toNumber(arg(args, 0)).rounded(.up)) }, + ] + return Dictionary(uniqueKeysWithValues: defs.map { ($0.name, $0) }) +}() + +/// Builtin names in declaration order, for prompt docs. +public let builtinOrder = [ + "Count", "First", "Last", "Sum", "Avg", "Min", "Max", "Sort", "Filter", "Round", "Abs", "Floor", + "Ceil", +] + +/// Builtins that receive AST nodes instead of evaluated values. +public let lazyBuiltins: Set = ["Each"] + +public let lazyBuiltinDefs: [String: (signature: String, description: String)] = [ + "Each": ( + "Each(array, varName, template)", + "Evaluate template for each element. varName is the loop variable — use it ONLY inside the template expression (inline). Do NOT create a separate statement for the template." + ) +] + +/// Parser-level action step names mapped to runtime step types. +public let actionSteps: [String: String] = [ + "Run": "run", + "ToAssistant": "continue_conversation", + "OpenUrl": "open_url", + "Set": "set", + "Reset": "reset", +] + +/// Action step names plus the `Action` container. +public let actionNames: Set = Set(["Action"]).union(actionSteps.keys) + +/// Every builtin name, including action expressions. +public let builtinNames: Set = Set(builtins.keys).union(lazyBuiltins).union(actionNames) + +public func isBuiltin(_ name: String) -> Bool { builtinNames.contains(name) } + +/// Statement-level calls that are neither builtins nor components. +public enum ReservedCall { + public static let query = "Query" + public static let mutation = "Mutation" +} + +public func isReservedCall(_ name: String) -> Bool { + name == ReservedCall.query || name == ReservedCall.mutation +} diff --git a/packages/swift-lang/Sources/OpenUILang/ChatComponents.swift b/packages/swift-lang/Sources/OpenUILang/ChatComponents.swift new file mode 100644 index 0000000000..3120dd23bd --- /dev/null +++ b/packages/swift-lang/Sources/OpenUILang/ChatComponents.swift @@ -0,0 +1,1094 @@ +// Generated by Scripts/generate-chat-components.mjs from openuiChatLibrary in +// @openuidev/react-ui. Don't edit by hand. +// swift-format-ignore-file: LineLength + +/// Schemas for the OpenUI chat library. Names, prop order, types and +/// descriptions match `openuiChatLibrary` in @openuidev/react-ui, so a backend +/// that prompts with either library produces responses both can render. +/// +/// The schemas live in `OpenUILang`, without SwiftUI, so a server can build +/// the chat prompt or Cloud config for them too. +public enum ChatComponents { + /// The structured `rules` object shared by form fields. + public static let formRules: PropType = .object( + ["required", "email", "url", "numeric"].map { Prop($0, .boolean, .optional) } + + ["min", "max", "minLength", "maxLength"].map { Prop($0, .number, .optional) } + + [Prop("pattern", .string, .optional)]) + + public static let card = ComponentSchema( + "Card", + description: + "Vertical container for all content in a chat response. Children stack top to bottom automatically. Optional sources ([{ title, sourceName, url }]) render as a Sources strip at the bottom and back inline [n] citations in TextContent.", + props: [ + Prop( + "children", + .array( + .union([ + .component("TextContent"), .component("MarkDownRenderer"), .component("CardHeader"), + .component("Callout"), .component("TextCallout"), .component("CodeBlock"), + .component("Image"), .component("ImageBlock"), .component("ImageGallery"), + .component("Separator"), .component("HorizontalBarChart"), .component("RadarChart"), + .component("PieChart"), .component("RadialChart"), .component("SingleStackedBarChart"), + .component("ScatterChart"), .component("AreaChart"), .component("BarChart"), + .component("LineChart"), .component("Table"), .component("TagBlock"), + .component("Form"), .component("Buttons"), .component("IconButton"), + .component("Steps"), .component("InlineHeader"), .component("EntityList"), + .component("EditableTable"), .component("SnippetCardBlock"), + .component("OverviewCardBlock"), .component("ContextCardBlock"), + .component("CompositeCardBlock"), .component("VisualCardBlock"), + .component("ListBlock"), .component("FollowUpBlock"), .component("SectionBlock"), + .component("Tabs"), .component("Carousel"), + ]))), + Prop( + "sources", + .array( + .object([ + Prop("url", .string, .optional), Prop("title", .string), Prop("sourceName", .string), + ])), .optional), + ]) + + public static let cardHeader = ComponentSchema( + "CardHeader", description: "Header with optional title and subtitle", + props: [Prop("title", .string, .optional), Prop("subtitle", .string, .optional)]) + + public static let textContent = ComponentSchema( + "TextContent", + description: + "Text block. Supports markdown. Optional size: \"small\" | \"default\" | \"large\" | \"small-heavy\" | \"large-heavy\".", + props: [ + Prop("text", .string), + Prop( + "size", .enumeration(["small", "default", "large", "small-heavy", "large-heavy"]), .optional + ), + ]) + + public static let markDownRenderer = ComponentSchema( + "MarkDownRenderer", description: "Renders markdown text with optional container variant", + props: [ + Prop("textMarkdown", .string), + Prop("variant", .enumeration(["clear", "card", "sunk"]), .optional), + ]) + + public static let callout = ComponentSchema( + "Callout", + description: + "Callout banner. Optional visible is a reactive $boolean — auto-dismisses after 3s by setting $visible to false.", + props: [ + Prop("variant", .enumeration(["info", "warning", "error", "success", "neutral"])), + Prop("title", .string), Prop("description", .string), + Prop("visible", .boolean, .optional, binding: true), + ]) + + public static let textCallout = ComponentSchema( + "TextCallout", description: "Text callout with variant, title, and description", + props: [ + Prop( + "variant", .enumeration(["neutral", "info", "warning", "success", "danger"]), .optional), + Prop("title", .string, .optional), Prop("description", .string, .optional), + ]) + + public static let image = ComponentSchema( + "Image", description: "Image with alt text and optional URL", + props: [Prop("alt", .string), Prop("src", .string, .optional)]) + + public static let imageBlock = ComponentSchema( + "ImageBlock", description: "Image block with loading state", + props: [Prop("src", .string), Prop("alt", .string, .optional)]) + + public static let imageGallery = ComponentSchema( + "ImageGallery", description: "Gallery grid of images with modal preview", + props: [ + Prop( + "images", + .array( + .object([ + Prop("src", .string), Prop("alt", .string, .optional), + Prop("details", .string, .optional), + ]))) + ]) + + public static let codeBlock = ComponentSchema( + "CodeBlock", description: "Syntax-highlighted code block", + props: [Prop("language", .string), Prop("codeString", .string)]) + + public static let separator = ComponentSchema( + "Separator", description: "Visual divider between content sections", + props: [ + Prop("orientation", .enumeration(["horizontal", "vertical"]), .optional), + Prop("decorative", .boolean, .optional), + ]) + + public static let table = ComponentSchema( + "Table", description: "Data table — column-oriented. Each Col holds its own data array.", + props: [Prop("columns", .array(.component("Col")))]) + + public static let col = ComponentSchema( + "Col", description: "Column definition — holds label + data array", + props: [ + Prop("label", .string), Prop("data", .any), + Prop("type", .enumeration(["string", "number", "action"]), .optional), + ]) + + public static let barChart = ComponentSchema( + "BarChart", + description: + "Vertical bars; use for comparing values across categories with one or more series", + props: [ + Prop("labels", .array(.string)), Prop("series", .array(.component("Series"))), + Prop("variant", .enumeration(["grouped", "stacked"]), .optional), + Prop("xLabel", .string, .optional), Prop("yLabel", .string, .optional), + Prop("height", .number, .optional), + ]) + + public static let lineChart = ComponentSchema( + "LineChart", description: "Lines over categories; use for trends and continuous data over time", + props: [ + Prop("labels", .array(.string)), Prop("series", .array(.component("Series"))), + Prop("variant", .enumeration(["linear", "natural", "step"]), .optional), + Prop("xLabel", .string, .optional), Prop("yLabel", .string, .optional), + Prop("height", .number, .optional), + ]) + + public static let areaChart = ComponentSchema( + "AreaChart", + description: "Filled area under lines; use for cumulative totals or volume trends over time", + props: [ + Prop("labels", .array(.string)), Prop("series", .array(.component("Series"))), + Prop("variant", .enumeration(["linear", "natural", "step"]), .optional), + Prop("xLabel", .string, .optional), Prop("yLabel", .string, .optional), + Prop("height", .number, .optional), + ]) + + public static let radarChart = ComponentSchema( + "RadarChart", + description: + "Spider/web chart; use for comparing multiple variables across one or more entities", + props: [Prop("labels", .array(.string)), Prop("series", .array(.component("Series")))]) + + public static let horizontalBarChart = ComponentSchema( + "HorizontalBarChart", + description: "Horizontal bars; prefer when category labels are long or for ranked lists", + props: [ + Prop("labels", .array(.string)), Prop("series", .array(.component("Series"))), + Prop("variant", .enumeration(["grouped", "stacked"]), .optional), + Prop("xLabel", .string, .optional), Prop("yLabel", .string, .optional), + ]) + + public static let series = ComponentSchema( + "Series", description: "One data series", + props: [Prop("category", .string), Prop("values", .array(.number))]) + + public static let pieChart = ComponentSchema( + "PieChart", + description: "Circular slices; use plucked arrays: PieChart(data.categories, data.values)", + props: [ + Prop("labels", .array(.string)), Prop("values", .array(.number)), + Prop("variant", .enumeration(["pie", "donut"]), .optional), + Prop("appearance", .enumeration(["circular", "semiCircular"]), .optional), + ]) + + public static let radialChart = ComponentSchema( + "RadialChart", + description: "Radial bars; use plucked arrays: RadialChart(data.categories, data.values)", + props: [Prop("labels", .array(.string)), Prop("values", .array(.number))]) + + public static let singleStackedBarChart = ComponentSchema( + "SingleStackedBarChart", + description: + "Single horizontal stacked bar; use plucked arrays: SingleStackedBarChart(data.categories, data.values)", + props: [Prop("labels", .array(.string)), Prop("values", .array(.number))]) + + public static let slice = ComponentSchema( + "Slice", description: "One slice with label and numeric value", + props: [Prop("category", .string), Prop("value", .number)]) + + public static let scatterChart = ComponentSchema( + "ScatterChart", + description: "X/Y scatter plot; use for correlations, distributions, and clustering", + props: [ + Prop("datasets", .array(.component("ScatterSeries"))), Prop("xLabel", .string, .optional), + Prop("yLabel", .string, .optional), + ]) + + public static let scatterSeries = ComponentSchema( + "ScatterSeries", description: "Named dataset", + props: [Prop("name", .string), Prop("points", .array(.component("Point")))]) + + public static let point = ComponentSchema( + "Point", description: "Data point with numeric coordinates", + props: [Prop("x", .number), Prop("y", .number), Prop("z", .number, .optional)]) + + public static let form = ComponentSchema( + "Form", description: "Form container with fields and explicit action buttons", + props: [ + Prop("name", .string), Prop("buttons", .component("Buttons")), + Prop("fields", .array(.component("FormControl")), .defaulted([])), + ]) + + public static let formControl = ComponentSchema( + "FormControl", description: "Field with label, input component, and optional hint text", + props: [ + Prop("label", .string), + Prop( + "input", + .union([ + .component("Input"), .component("TextArea"), .component("Select"), + .component("DatePicker"), .component("Slider"), .component("CheckBoxGroup"), + .component("RadioGroup"), .component("Chips"), .component("OptionCards"), + ])), Prop("hint", .string, .optional), + ]) + + public static let label = ComponentSchema( + "Label", description: "Text label", + props: [Prop("text", .string)]) + + public static let input = ComponentSchema( + "Input", description: "", + props: [ + Prop("name", .string), Prop("placeholder", .string, .optional), + Prop("type", .enumeration(["text", "email", "password", "number", "url"]), .optional), + Prop("rules", formRules, .optional), Prop("value", .string, .optional, binding: true), + ]) + + public static let textArea = ComponentSchema( + "TextArea", description: "", + props: [ + Prop("name", .string), Prop("placeholder", .string, .optional), + Prop("rows", .number, .optional), Prop("rules", formRules, .optional), + Prop("value", .string, .optional, binding: true), + ]) + + public static let select = ComponentSchema( + "Select", description: "", + props: [ + Prop("name", .string), Prop("items", .array(.component("SelectItem"))), + Prop("placeholder", .string, .optional), Prop("rules", formRules, .optional), + Prop("value", .string, .optional, binding: true), + Prop("size", .enumeration(["small", "medium", "large"]), .optional), + ]) + + public static let selectItem = ComponentSchema( + "SelectItem", description: "Option for Select", + props: [Prop("value", .string), Prop("label", .string)]) + + public static let datePicker = ComponentSchema( + "DatePicker", description: "", + props: [ + Prop("name", .string), Prop("mode", .enumeration(["single", "range"]), .optional), + Prop("rules", formRules, .optional), Prop("value", .any, .optional, binding: true), + ]) + + public static let slider = ComponentSchema( + "Slider", + description: "Numeric slider input; supports continuous and discrete (stepped) variants", + props: [ + Prop("name", .string), Prop("variant", .enumeration(["continuous", "discrete"])), + Prop("min", .number), Prop("max", .number), Prop("step", .number, .optional), + Prop("defaultValue", .array(.number), .optional), Prop("label", .string, .optional), + Prop("rules", formRules, .optional), Prop("value", .array(.number), .optional, binding: true), + ]) + + public static let checkBoxGroup = ComponentSchema( + "CheckBoxGroup", description: "", + props: [ + Prop("name", .string), Prop("items", .array(.component("CheckBoxItem"))), + Prop("rules", formRules, .optional), + Prop("value", .record(.boolean), .optional, binding: true), + ]) + + public static let checkBoxItem = ComponentSchema( + "CheckBoxItem", description: "", + props: [ + Prop("label", .string), Prop("description", .string), Prop("name", .string), + Prop("defaultChecked", .boolean, .optional), + ]) + + public static let radioGroup = ComponentSchema( + "RadioGroup", description: "", + props: [ + Prop("name", .string), Prop("items", .array(.component("RadioItem"))), + Prop("defaultValue", .string, .optional), Prop("rules", formRules, .optional), + Prop("value", .string, .optional, binding: true), + ]) + + public static let radioItem = ComponentSchema( + "RadioItem", description: "", + props: [Prop("label", .string), Prop("description", .string), Prop("value", .string)]) + + public static let switchGroup = ComponentSchema( + "SwitchGroup", description: "Group of switch toggles", + props: [ + Prop("name", .string), Prop("items", .array(.component("SwitchItem"))), + Prop("variant", .enumeration(["clear", "card", "sunk"]), .optional), + Prop("value", .record(.boolean), .optional, binding: true), + ]) + + public static let switchItem = ComponentSchema( + "SwitchItem", description: "Individual switch toggle", + props: [ + Prop("label", .string, .optional), Prop("description", .string, .optional), + Prop("name", .string), Prop("defaultChecked", .boolean, .optional), + ]) + + public static let button = ComponentSchema( + "Button", description: "Clickable button", + props: [ + Prop("label", .string), Prop("action", .actionExpression, .optional), + Prop("variant", .enumeration(["primary", "secondary", "tertiary"]), .optional), + Prop("type", .enumeration(["normal", "destructive"]), .optional), + Prop("size", .enumeration(["extra-small", "small", "medium", "large"]), .optional), + ]) + + public static let buttons = ComponentSchema( + "Buttons", + description: "Group of Button components. direction: \"row\" (default) | \"column\".", + props: [ + Prop("buttons", .array(.component("Button"))), + Prop("direction", .enumeration(["row", "column"]), .optional), + ]) + + public static let listBlock = ComponentSchema( + "ListBlock", + description: + "A list of items with number or image indicators. Each item can optionally have an action. size small renders a compact list.", + props: [ + Prop("items", .array(.component("ListItem"))), + Prop("variant", .enumeration(["number", "image"]), .optional), + Prop("size", .enumeration(["default", "small"]), .optional), + ]) + + public static let listItem = ComponentSchema( + "ListItem", + description: + "Item in a ListBlock — displays a title with an optional subtitle and image. When action is provided, the item becomes clickable.", + props: [ + Prop("title", .string), Prop("subtitle", .string, .optional), + Prop("image", .object([Prop("src", .string), Prop("alt", .string)]), .optional), + Prop("actionLabel", .string, .optional), Prop("action", .actionExpression, .optional), + ]) + + public static let followUpBlock = ComponentSchema( + "FollowUpBlock", + description: "List of clickable follow-up suggestions placed at the end of a response", + props: [Prop("items", .array(.component("FollowUpItem")))]) + + public static let followUpItem = ComponentSchema( + "FollowUpItem", + description: "Clickable follow-up suggestion — when clicked, sends text as user message", + props: [Prop("text", .string)]) + + public static let sectionBlock = ComponentSchema( + "SectionBlock", + description: + "Collapsible accordion sections. Auto-opens sections as they stream in. Use SectionItem for each section.", + props: [ + Prop("sections", .array(.component("SectionItem"))), Prop("isFoldable", .boolean, .optional), + ]) + + public static let sectionItem = ComponentSchema( + "SectionItem", + description: "Section with a label and collapsible content — used inside SectionBlock", + props: [ + Prop("value", .string), Prop("trigger", .string), + Prop( + "content", + .array( + .union([ + .component("TextContent"), .component("MarkDownRenderer"), .component("CardHeader"), + .component("Callout"), .component("TextCallout"), .component("CodeBlock"), + .component("Image"), .component("ImageBlock"), .component("ImageGallery"), + .component("Separator"), .component("HorizontalBarChart"), .component("RadarChart"), + .component("PieChart"), .component("RadialChart"), .component("SingleStackedBarChart"), + .component("ScatterChart"), .component("AreaChart"), .component("BarChart"), + .component("LineChart"), .component("Table"), .component("TagBlock"), + .component("Form"), .component("Buttons"), .component("IconButton"), + .component("Steps"), .component("InlineHeader"), .component("EntityList"), + .component("EditableTable"), .component("SnippetCardBlock"), + .component("OverviewCardBlock"), .component("ContextCardBlock"), + .component("CompositeCardBlock"), .component("VisualCardBlock"), + .component("ListBlock"), .component("FollowUpBlock"), .component("Tabs"), + .component("Accordion"), + ]))), + ]) + + public static let tabs = ComponentSchema( + "Tabs", description: "Tabbed container", + props: [Prop("items", .array(.component("TabItem")))]) + + public static let tabItem = ComponentSchema( + "TabItem", + description: "value is unique id, trigger is tab label, content is array of components", + props: [ + Prop("value", .string), Prop("trigger", .string), + Prop( + "content", + .array( + .union([ + .component("TextContent"), .component("MarkDownRenderer"), .component("CardHeader"), + .component("Callout"), .component("TextCallout"), .component("CodeBlock"), + .component("Image"), .component("ImageBlock"), .component("ImageGallery"), + .component("Separator"), .component("HorizontalBarChart"), .component("RadarChart"), + .component("PieChart"), .component("RadialChart"), .component("SingleStackedBarChart"), + .component("ScatterChart"), .component("AreaChart"), .component("BarChart"), + .component("LineChart"), .component("Table"), .component("TagBlock"), + .component("Form"), .component("Buttons"), .component("IconButton"), + .component("Steps"), .component("InlineHeader"), .component("EntityList"), + .component("EditableTable"), .component("SnippetCardBlock"), + .component("OverviewCardBlock"), .component("ContextCardBlock"), + .component("CompositeCardBlock"), .component("VisualCardBlock"), + .component("ListBlock"), .component("FollowUpBlock"), .component("Accordion"), + ]))), + ]) + + public static let accordion = ComponentSchema( + "Accordion", description: "Collapsible sections", + props: [Prop("items", .array(.component("AccordionItem")))]) + + public static let accordionItem = ComponentSchema( + "AccordionItem", description: "value is unique id, trigger is section title", + props: [ + Prop("value", .string), Prop("trigger", .string), + Prop( + "content", + .array( + .union([ + .component("TextContent"), .component("MarkDownRenderer"), .component("CardHeader"), + .component("Callout"), .component("TextCallout"), .component("CodeBlock"), + .component("Image"), .component("ImageBlock"), .component("ImageGallery"), + .component("Separator"), .component("HorizontalBarChart"), .component("RadarChart"), + .component("PieChart"), .component("RadialChart"), .component("SingleStackedBarChart"), + .component("ScatterChart"), .component("AreaChart"), .component("BarChart"), + .component("LineChart"), .component("Table"), .component("TagBlock"), + .component("Form"), .component("Buttons"), .component("IconButton"), + .component("Steps"), .component("InlineHeader"), .component("EntityList"), + .component("EditableTable"), .component("SnippetCardBlock"), + .component("OverviewCardBlock"), .component("ContextCardBlock"), + .component("CompositeCardBlock"), .component("VisualCardBlock"), + .component("ListBlock"), .component("FollowUpBlock"), + ]))), + ]) + + public static let steps = ComponentSchema( + "Steps", description: "Step-by-step guide", + props: [Prop("items", .array(.component("StepsItem")))]) + + public static let stepsItem = ComponentSchema( + "StepsItem", description: "title and details text for one step", + props: [Prop("title", .string), Prop("details", .string)]) + + public static let carousel = ComponentSchema( + "Carousel", description: "Horizontal scrollable carousel", + props: [ + Prop( + "children", + .array( + .array( + .union([ + .component("TextContent"), .component("MarkDownRenderer"), .component("CardHeader"), + .component("Callout"), .component("TextCallout"), .component("CodeBlock"), + .component("Image"), .component("ImageBlock"), .component("ImageGallery"), + .component("Separator"), .component("HorizontalBarChart"), .component("RadarChart"), + .component("PieChart"), .component("RadialChart"), + .component("SingleStackedBarChart"), .component("ScatterChart"), + .component("AreaChart"), .component("BarChart"), .component("LineChart"), + .component("Table"), .component("TagBlock"), .component("Form"), + .component("Buttons"), .component("IconButton"), .component("Steps"), + .component("InlineHeader"), .component("EntityList"), .component("EditableTable"), + .component("SnippetCardBlock"), .component("OverviewCardBlock"), + .component("ContextCardBlock"), .component("CompositeCardBlock"), + .component("VisualCardBlock"), .component("ListBlock"), .component("FollowUpBlock"), + ])))), Prop("variant", .enumeration(["card", "sunk"]), .optional), + ]) + + public static let tagBlock = ComponentSchema( + "TagBlock", description: "tags is an array of strings; optional size sm | md | lg", + props: [ + Prop("tags", .array(.string)), Prop("size", .enumeration(["sm", "md", "lg"]), .optional), + ]) + + public static let tag = ComponentSchema( + "Tag", description: "Styled tag/badge with optional Icon and variant", + props: [ + Prop("text", .string), Prop("icon", .component("Icon"), .optional), + Prop("size", .enumeration(["sm", "md", "lg"]), .optional), + Prop( + "variant", .enumeration(["neutral", "info", "success", "warning", "danger"]), .optional), + ]) + + public static let entityList = ComponentSchema( + "EntityList", + description: + "Two-column key/value rows (left label, right value). size 'default' supports optional header and footer rows; rightVariant 'number' uses tabular numbers.", + props: [ + Prop( + "rows", + .array( + .object([ + Prop("left", .string), Prop("right", .string), + Prop("rightVariant", .enumeration(["text", "number"]), .defaulted("text")), + ])), .defaulted([])), + Prop("size", .enumeration(["small", "default"]), .defaulted("default")), + Prop( + "header", + .object([ + Prop("left", .string), Prop("right", .string), + Prop("rightVariant", .enumeration(["text", "number"]), .defaulted("text")), + ]), .optional), + Prop( + "footer", + .object([ + Prop("left", .string), Prop("right", .string), + Prop("rightVariant", .enumeration(["text", "number"]), .defaulted("text")), + ]), .optional), + ]) + + public static let inlineHeader = ComponentSchema( + "InlineHeader", + description: + "Compact section heading with an optional one-line description, for use inside cards.", + props: [Prop("heading", .string), Prop("description", .string, .optional)]) + + public static let icon = ComponentSchema( + "Icon", + description: + "A lucide icon by kebab-case name (e.g. 'circle-check'). Optional category picks a topical fallback when the name doesn't resolve.", + props: [Prop("name", .string), Prop("category", .string, .optional)]) + + public static let iconButton = ComponentSchema( + "IconButton", + description: + "Icon-only button. name is the accessible label and the action label; icon is an Icon; action fires on click.", + props: [ + Prop("name", .string), Prop("icon", .component("Icon")), + Prop("action", .actionExpression, .optional), + Prop("variant", .enumeration(["primary", "secondary", "tertiary"]), .optional), + Prop("size", .enumeration(["extra-small", "small", "medium", "large"]), .optional), + Prop("shape", .enumeration(["square", "circle"]), .optional), + ]) + + public static let editableTable = ComponentSchema( + "EditableTable", + description: + "Spreadsheet-like table whose cells the user can edit inline (text, number, url, date, select columns); edits are saved back as a form field", + props: [ + Prop("name", .string, .defaulted("")), + Prop( + "columns", + .array( + .object([ + Prop("type", .enumeration(["text", "number", "date-single", "select", "url"])), + Prop("key", .string, .defaulted("default")), Prop("header", .string, .defaulted("")), + Prop("width", .number, .optional), + Prop( + "options", .array(.object([Prop("value", .string), Prop("label", .string)])), + .optional), + ])), .defaulted([])), + Prop( + "data", + .array( + .object([Prop("id", .string), Prop("values", .array(.union([.string, .number])))])), + .defaulted([])), + ]) + + public static let chipItem = ComponentSchema( + "ChipItem", + description: + "A single selectable chip inside a Chips group, with a value, label and optional icon.", + props: [ + Prop("value", .string), Prop("label", .string), Prop("icon", .component("Icon"), .optional), + Prop("disabled", .boolean, .optional), + ]) + + public static let chips = ComponentSchema( + "Chips", + description: + "A form field of compact selectable chips for choosing one or many short options; the selection is stored under `name`.", + props: [ + Prop("name", .string), + Prop("type", .enumeration(["single", "multiple"]), .defaulted("multiple")), + Prop("items", .array(.component("ChipItem")), .defaulted([])), + Prop("rules", formRules, .optional), + Prop("defaultValue", .union([.string, .array(.string)]), .optional), + ]) + + public static let optionCard = ComponentSchema( + "OptionCard", + description: + "A single selectable card inside an OptionCards group, with a value, title, optional subtitle and an optional Icon or Image on top.", + props: [ + Prop("value", .string), Prop("title", .string), Prop("subtitle", .string, .optional), + Prop("topContent", .union([.component("Icon"), .component("Image")]), .optional), + Prop("disabled", .boolean, .optional), + ]) + + public static let optionCards = ComponentSchema( + "OptionCards", + description: + "A form field of selectable cards (title, optional subtitle, optional icon or image) laid out in a responsive grid for choosing one or many options; the selection is stored under `name`.", + props: [ + Prop("name", .string), + Prop("type", .enumeration(["single", "multiple"]), .defaulted("single")), + Prop("items", .array(.component("OptionCard")), .defaulted([])), + Prop("rules", formRules, .optional), + Prop("defaultValue", .union([.string, .array(.string)]), .optional), + ]) + + public static let text = ComponentSchema( + "Text", + description: + "Plain text line with optional subtext. variant 'number' uses tabular number styling; subtextVariant 'metric' colors a leading +/- subtext green/red.", + props: [ + Prop("variant", .enumeration(["text", "number"]), .defaulted("text")), Prop("value", .string), + Prop("subtext", .string, .optional), + Prop("subtextVariant", .enumeration(["text", "number", "metric"]), .defaulted("text")), + Prop("size", .enumeration(["xs", "sm", "md", "lg"]), .defaulted("sm")), + ]) + + public static let boldText = ComponentSchema( + "BoldText", + description: + "Emphasized (bold) text line with optional subtext. variant 'number' uses tabular number styling; subtextVariant 'metric' colors a leading +/- subtext green/red.", + props: [ + Prop("variant", .enumeration(["text", "number"]), .defaulted("text")), Prop("value", .string), + Prop("subtext", .string, .optional), + Prop("subtextVariant", .enumeration(["text", "number", "metric"]), .defaulted("text")), + Prop("size", .enumeration(["xs", "sm", "md", "lg"]), .defaulted("sm")), + ]) + + public static let iconText = ComponentSchema( + "IconText", + description: + "An icon badge with a title and optional subtitle, laid out horizontally or vertically. iconVariant sets the badge color.", + props: [ + Prop("icon", .component("Icon")), + Prop( + "iconVariant", + .enumeration([ + "neutral", "info", "success", "warning", "danger", "inverted", "filled", "soft", + ]), .defaulted("neutral")), + Prop( + "iconSize", .enumeration(["xs", "s", "m", "l", "xl", "sm", "md", "lg"]), .defaulted("m")), + Prop("title", .string), Prop("subtitle", .string, .optional), + Prop("bold", .boolean, .defaulted(false)), + Prop("layout", .enumeration(["horizontal", "vertical"]), .defaulted("horizontal")), + ]) + + public static let imageText = ComponentSchema( + "ImageText", + description: + "A small square image (thumbnail/avatar) with a title and optional subtitle. src must be a real image URL.", + props: [ + Prop("src", .string), Prop("alt", .string, .optional), Prop("title", .string), + Prop("subtitle", .string, .optional), Prop("bold", .boolean, .defaulted(false)), + Prop("layout", .enumeration(["horizontal", "vertical"]), .defaulted("horizontal")), + Prop("imageSize", .number, .optional), + ]) + + public static let imageTextLarge = ComponentSchema( + "ImageTextLarge", + description: + "A full-width banner image above a bold title and optional subtitle. src must be a real image URL.", + props: [ + Prop("src", .string), Prop("alt", .string, .optional), Prop("title", .string), + Prop("subtitle", .string, .optional), Prop("bold", .boolean, .defaulted(false)), + ]) + + public static let metricIndicatorInline = ComponentSchema( + "MetricIndicatorInline", + description: + "Headline metric value with an optional +/- percentage trend and subtext, all on one line.", + props: [ + Prop("value", .string), Prop("subtext", .string, .optional), + Prop( + "trend", + .object([Prop("direction", .enumeration(["up", "down"])), Prop("value", .number)]), + .optional), + ]) + + public static let metricIndicatorWithStrikethrough = ComponentSchema( + "MetricIndicatorWithStrikethrough", + description: + "Headline metric value with an optional struck-through previousValue, a +/- percentage trend, and subtext below.", + props: [ + Prop("value", .string), Prop("subtext", .string, .optional), + Prop("previousValue", .string, .optional), + Prop( + "trend", + .object([Prop("direction", .enumeration(["up", "down"])), Prop("value", .number)]), + .optional), + ]) + + public static let snippetCardItem = ComponentSchema( + "SnippetCardItem", + description: + "One row-style snippet card: a label on the left (IconText or ImageText) and an optional value on the right (Text or BoldText).", + props: [ + Prop("id", .string, .optional), + Prop("lhs", .union([.component("IconText"), .component("ImageText")])), + Prop("rhs", .union([.component("Text"), .component("BoldText")]), .optional), + ]) + + public static let snippetCardBlock = ComponentSchema( + "SnippetCardBlock", + description: + "A responsive grid of compact label/value cards (2 per row) for showing several short facts side by side; optionally clickable with a shared action.", + props: [ + Prop("items", .array(.component("SnippetCardItem"), minItems: 2)), + Prop("layout", .enumeration(["grid"]), .defaulted("grid")), + Prop("responsive", .boolean, .defaulted(true)), Prop("action", .actionExpression, .optional), + Prop("gap", .union([.number, .string]), .optional), + ]) + + public static let overviewCardItem = ComponentSchema( + "OverviewCardItem", + description: + "One overview card: a heading slot at the top (IconText, ImageText or Text) and an optional MetricIndicatorInline at the bottom.", + props: [ + Prop("id", .string, .optional), + Prop("top", .union([.component("IconText"), .component("ImageText"), .component("Text")])), + Prop("bottom", .component("MetricIndicatorInline"), .optional), + ]) + + public static let overviewCardBlock = ComponentSchema( + "OverviewCardBlock", + description: + "A grid or horizontal carousel of compact overview cards, each with a heading (icon/image/text) on top and an inline metric below; optionally clickable with a shared action.", + props: [ + Prop("items", .array(.component("OverviewCardItem"), minItems: 2)), + Prop("layout", .enumeration(["grid", "carousel"]), .defaulted("grid")), + Prop("responsive", .boolean, .defaulted(true)), Prop("action", .actionExpression, .optional), + Prop("gap", .union([.number, .string]), .optional), + ]) + + public static let contextCardItem = ComponentSchema( + "ContextCardItem", + description: + "A single card inside a ContextCardBlock: a title (plain string or Tag), an optional markdown body, and an optional gray tint or background image.", + props: [ + Prop("id", .string, .optional), Prop("title", .union([.string, .component("Tag")])), + Prop("body", .string, .optional), Prop("bgColor", .enumeration(["gray"]), .optional), + Prop("bgImageSrc", .string, .optional), Prop("bgImageAlt", .string, .optional), + ]) + + public static let contextCardBlock = ComponentSchema( + "ContextCardBlock", + description: + "A grid or carousel of compact tinted context cards (title or tag plus a short bold body); an optional action makes every card clickable.", + props: [ + Prop("items", .array(.component("ContextCardItem"), minItems: 2)), + Prop("layout", .enumeration(["grid", "carousel"]), .defaulted("grid")), + Prop("responsive", .boolean, .defaulted(true)), Prop("action", .actionExpression, .optional), + Prop("gap", .union([.number, .string]), .optional), + ]) + + public static let compositeCardItem = ComponentSchema( + "CompositeCardItem", + description: + "A single card inside a CompositeCardBlock: an optional header (icon/image/text), a stack of body elements (text, metrics, charts, lists, tags), and an optional price/button footer.", + props: [ + Prop("id", .string, .optional), + Prop( + "header", + .union([ + .component("IconText"), .component("ImageText"), .component("ImageTextLarge"), + .component("Text"), .component("Image"), + ]), .optional), + Prop( + "body", + .array( + .union([ + .component("Text"), .component("BoldText"), .component("MetricIndicatorInline"), + .component("IconText"), .component("Image"), .component("AreaChart"), + .component("BarChart"), .component("LineChart"), .component("ListBlock"), + .component("TagBlock"), .component("EntityList"), + ])), .defaulted([])), + Prop( + "footer", + .object([ + Prop( + "price", + .union([.component("BoldText"), .component("MetricIndicatorWithStrikethrough")]), + .optional), Prop("button", .component("Button"), .optional), + ]), .optional), + ]) + + public static let compositeCardBlock = ComponentSchema( + "CompositeCardBlock", + description: + "A two-per-row grid or carousel of rich cards, each with an optional header, stacked body content (text, metrics, charts, lists, tags) and a price/button footer; an optional action makes every card clickable.", + props: [ + Prop("items", .array(.component("CompositeCardItem"), minItems: 2)), + Prop("layout", .enumeration(["grid", "carousel"]), .defaulted("grid")), + Prop("responsive", .boolean, .defaulted(true)), Prop("action", .actionExpression, .optional), + Prop("gap", .union([.number, .string]), .optional), + ]) + + public static let visualCardItem = ComponentSchema( + "VisualCardItem", + description: + "A single photo-first card inside a VisualCardBlock: a BoldText body panel, an optional Tag, and a background image (bgImageSrc must be a real URL; bgImageAlt is its alt text).", + props: [ + Prop("body", .component("BoldText")), Prop("id", .string, .optional), + Prop("bgImageSrc", .string, .optional), Prop("tag", .component("Tag"), .optional), + Prop("bgImageAlt", .string, .optional), + ]) + + public static let visualCardBlock = ComponentSchema( + "VisualCardBlock", + description: + "A grid or carousel of photo-first cards: a full-bleed background image with a tag on top and a bold text panel at the bottom; an optional action makes every card clickable.", + props: [ + Prop("items", .array(.component("VisualCardItem"), minItems: 2)), + Prop("layout", .enumeration(["grid", "carousel"]), .defaulted("grid")), + Prop("responsive", .boolean, .defaulted(true)), Prop("action", .actionExpression, .optional), + Prop("gap", .union([.number, .string]), .optional), + ]) + + /// Prompt groups and notes, as in openuiChatLibrary. + public static let groups: [ComponentGroup] = [ + ComponentGroup( + name: "Content", + components: [ + "CardHeader", "TextContent", "MarkDownRenderer", "Callout", "TextCallout", "Image", + "ImageBlock", "ImageGallery", "CodeBlock", "Separator", "InlineHeader", + ], + notes: [ + "- InlineHeader is a compact heading + description pair for labelling a block inside the response (lighter than CardHeader).", + "- Pass sources on Card ([{ title, sourceName, url }]) when the answer relies on references, and cite them inline in TextContent as [1], [2] (1-based index into sources). A Sources strip renders at the bottom of the card.", + ]), + ComponentGroup( + name: "Tables", components: ["Table", "Col", "EditableTable"], + notes: [ + "- EditableTable lets the user edit cells inline. Give it a unique name, columns of { type, key, header } with type one of text | number | date-single | select | url (select also needs options: [{ value, label }]).", + "- data is an array of { id, values } rows where values are ordered positionally to match columns. Edited data is submitted when the user clicks Save Changes.", + ]), + ComponentGroup( + name: "Charts (2D)", + components: [ + "BarChart", "LineChart", "AreaChart", "RadarChart", "HorizontalBarChart", "Series", + ]), + ComponentGroup( + name: "Charts (1D)", + components: ["PieChart", "RadialChart", "SingleStackedBarChart", "Slice"]), + ComponentGroup( + name: "Charts (Scatter)", components: ["ScatterChart", "ScatterSeries", "Point"]), + ComponentGroup( + name: "Forms", + components: [ + "Form", "FormControl", "Label", "Input", "TextArea", "Select", "SelectItem", "DatePicker", + "Slider", "CheckBoxGroup", "CheckBoxItem", "RadioGroup", "RadioItem", "SwitchGroup", + "SwitchItem", "Chips", "ChipItem", "OptionCards", "OptionCard", + ], + notes: [ + "- Chips: compact single/multiple selection pills. Use ChipItem references for each option.", + "- OptionCards: larger selectable cards with title, subtitle and an optional Icon or Image on top. Use OptionCard references for each option.", + "- Define EACH FormControl as its own reference — do NOT inline all controls in one array.", + "- NEVER nest Form inside Form.", + "- Form requires explicit buttons. Always pass a Buttons(...) reference as the second Form argument: Form(name, buttons, fields).", + "- rules is an optional object: { required: true, email: true, min: 8, maxLength: 100 }", + "- The renderer shows error messages automatically — do NOT generate error text in the UI", + ]), + ComponentGroup( + name: "Buttons", components: ["Button", "Buttons", "Icon", "IconButton"], + notes: [ + "- Icon renders a lucide icon by kebab-case name; it is also used as the icon of IconButton, IconText and OptionCard." + ]), + ComponentGroup( + name: "Lists & Follow-ups", + components: ["ListBlock", "ListItem", "FollowUpBlock", "FollowUpItem"], + notes: [ + "- Use ListBlock with ListItem references for numbered lists.", + "- Use FollowUpBlock with FollowUpItem references at the end of a response to suggest next actions.", + "- A ListItem is clickable ONLY when given an action (5th argument); without one it is plain text.", + "- Clicking a FollowUpItem, or a ListItem with a continue_conversation action, sends text to the LLM as a user message.", + "- Example: list = ListBlock([item1, item2]) item1 = ListItem(\"Option A\", \"Details about A\", null, null, { type: \"continue_conversation\", context: \"Option A\" })", + ]), + ComponentGroup( + name: "Sections", components: ["SectionBlock", "SectionItem"], + notes: [ + "- SectionBlock renders collapsible accordion sections that auto-open as they stream.", + "- Each section needs a unique `value` id, a `trigger` label, and a `content` array.", + "- Example: sections = SectionBlock([s1, s2]) s1 = SectionItem(\"intro\", \"Introduction\", [content1])", + "- Set isFoldable=false to render sections as flat headers instead of accordion.", + ]), + ComponentGroup( + name: "Layout", + components: [ + "Tabs", "TabItem", "Accordion", "AccordionItem", "Steps", "StepsItem", "Carousel", + ], + notes: [ + "- Use Tabs to present alternative views — each TabItem has a value id, trigger label, and content array.", + "- Carousel takes an array of slides, where each slide is an array of content: carousel = Carousel([[t1, img1], [t2, img2]])", + "- IMPORTANT: Every slide in a Carousel must have the same structure — same component types in the same order.", + "- For image carousels use: [[title, image, description, tags], ...] — every slide must follow this exact pattern.", + "- Use real, publicly accessible image URLs (e.g. https://picsum.photos/seed/KEYWORD/800/500). Never hallucinate image URLs.", + ]), + ComponentGroup( + name: "Data Display", components: ["TagBlock", "Tag", "EntityList"], + notes: [ + "- EntityList is a compact two-column list of { left, right } rows (e.g. name / value). size='default' also supports a header and footer row; size='small' does not." + ]), + ComponentGroup( + name: "Cards", + components: [ + "SnippetCardBlock", "SnippetCardItem", "OverviewCardBlock", "OverviewCardItem", + "ContextCardBlock", "ContextCardItem", "CompositeCardBlock", "CompositeCardItem", + "VisualCardBlock", "VisualCardItem", "Text", "BoldText", "IconText", "ImageText", + "ImageTextLarge", "MetricIndicatorInline", "MetricIndicatorWithStrikethrough", + ], + notes: [ + "- Card blocks lay out 2+ items in a responsive grid (or carousel where supported). Every item in a block must have the same structure.", + "- SnippetCardItem: small card with lhs (IconText | ImageText) and optional rhs (Text | BoldText) — good for key/value facts.", + "- OverviewCardItem: small card with top (IconText | ImageText | Text) and optional bottom MetricIndicatorInline — good for KPIs.", + "- ContextCardItem: medium card with a title (string or Tag), body text and optional background image — good for summaries.", + "- CompositeCardItem: rich card with header, body array (Text, BoldText, MetricIndicatorInline, IconText, Image, charts, ListBlock, TagBlock, EntityList) and footer (price + Button) — good for products/offers.", + "- VisualCardItem: image-first card with a BoldText body and optional Tag.", + "- Text / BoldText / IconText / ImageText / ImageTextLarge / MetricIndicator* are the inline building blocks used INSIDE card items; do not place them directly in the root Card.", + ]), + ] + + /// Every schema, in library order. + public static let all: [ComponentSchema] = [ + card, + cardHeader, + textContent, + markDownRenderer, + callout, + textCallout, + image, + imageBlock, + imageGallery, + codeBlock, + separator, + table, + col, + barChart, + lineChart, + areaChart, + radarChart, + horizontalBarChart, + series, + pieChart, + radialChart, + singleStackedBarChart, + slice, + scatterChart, + scatterSeries, + point, + form, + formControl, + label, + input, + textArea, + select, + selectItem, + datePicker, + slider, + checkBoxGroup, + checkBoxItem, + radioGroup, + radioItem, + switchGroup, + switchItem, + button, + buttons, + listBlock, + listItem, + followUpBlock, + followUpItem, + sectionBlock, + sectionItem, + tabs, + tabItem, + accordion, + accordionItem, + steps, + stepsItem, + carousel, + tagBlock, + tag, + entityList, + inlineHeader, + icon, + iconButton, + editableTable, + chipItem, + chips, + optionCard, + optionCards, + text, + boldText, + iconText, + imageText, + imageTextLarge, + metricIndicatorInline, + metricIndicatorWithStrikethrough, + snippetCardItem, + snippetCardBlock, + overviewCardItem, + overviewCardBlock, + contextCardItem, + contextCardBlock, + compositeCardItem, + compositeCardBlock, + visualCardItem, + visualCardBlock, + ] + + /// Example responses for the prompt (`openuiChatExamples`). + public static let examples: [String] = [ + "Example 1 — Informational response (the shape for \"tell me about X\" / explainer asks):\n\nroot = Card([header, intro, highlights, statsHeader, statsCards, sectionsBlock, actionBtns], [src1, src2])\nheader = CardHeader(\"Paris\", \"The City of Light · France's Eternal Capital\")\nintro = TextContent(\"Paris is one of the world's most visited cities — a timeless blend of iconic landmarks, world-class cuisine, art, and romance [1]. In 2026 the city is buzzing with new energy: the **Grand Palais** has reopened after a €466M renovation, and you can now **swim in the Seine** for the first time in over a century [2].\")\nhighlights = ContextCardBlock([h1, h2, h3], \"grid\", true, { type: \"continue_conversation\", context: \"Tell me more about this aspect of Paris\" })\nh1 = ContextCardItem(\"art-museums\", \"Art & Museums\", \"143 museums including the Louvre & newly reopened Grand Palais.\", \"gray\", \"https://cdn.britannica.com/03/121003-050-2544BD4E/Interior-Louvre-Museum-Paris.jpg\")\nh2 = ContextCardItem(\"gastronomy\", \"Gastronomy\", \"9,000+ restaurants, 130+ Michelin stars, and the world's best baguettes.\", \"gray\")\nh3 = ContextCardItem(\"fashion-capital\", \"Fashion Capital\", \"Home to Chanel, Dior, Louis Vuitton & Hermès — the global luxury hub.\", \"gray\", \"https://thumbs.dreamstime.com/z/dior-storefront-facade-chanel-store-lvmh-s-french-designer-modehouse-christian-adjacent-to-luxury-fashion-beauty-brand-327161553.jpg\")\nstatsHeader = InlineHeader(\"Paris by the Numbers\", \"Key visitor statistics\")\nstatsCards = OverviewCardBlock([ov1, ov2, ov3], \"grid\", true)\nov1 = OverviewCardItem(\"annual-visitors\", ov1top, ov1metric)\nov1top = IconText(ov1icon, \"neutral\", \"m\", \"Annual Visitors\", \"2024 arrivals\", false, \"vertical\")\nov1icon = Icon(\"users\", \"people\")\nov1metric = MetricIndicatorInline(\"48.7M\", \"tourists\", { direction: \"up\", value: 2.5 })\nov2 = OverviewCardItem(\"museums-monuments\", ov2top, ov2metric)\nov2top = IconText(ov2icon, \"neutral\", \"m\", \"Museums & Monuments\", \"Cultural sites\", false, \"vertical\")\nov2icon = Icon(\"landmark\", \"buildings\")\nov2metric = MetricIndicatorInline(\"2,370+\", \"across the city\")\nov3 = OverviewCardItem(\"tourism-revenue\", ov3top, ov3metric)\nov3top = IconText(ov3icon, \"neutral\", \"m\", \"Tourism Revenue\", \"2024 estimate\", false, \"vertical\")\nov3icon = Icon(\"circle-dollar-sign\", \"finance\")\nov3metric = MetricIndicatorInline(\"€23.4B\", \"generated\")\nsectionsBlock = SectionBlock([secLandmarks, secFood, secWhen], false)\nsecLandmarks = SectionItem(\"landmarks\", \"Iconic Landmarks\", [landmarksCarousel])\nlandmarksCarousel = VisualCardBlock([lm1, lm2, lm3, lm4], \"carousel\", true, { type: \"continue_conversation\", context: \"Tell me more about this Paris landmark\" })\nlm1 = VisualCardItem(lm1body, \"eiffel-tower\", \"https://c8.alamy.com/comp/RYEB13/aerial-view-of-the-eiffel-tower-with-the-park-champ-de-mars-and-the-river-seine-paris-france-RYEB13.jpg\", lm1tag, \"Aerial view of the Eiffel Tower\")\nlm1body = BoldText(\"text\", \"Eiffel Tower\", \"Open daily 9am–midnight\")\nlm1tag = Tag(\"Must-See\", lm1tagIcon, \"sm\", \"info\")\nlm1tagIcon = Icon(\"star\", \"shapes\")\nlm2 = VisualCardItem(lm2body, \"the-louvre\", \"https://images.fineartamerica.com/images/artworkimages/mediumlarge/3/1-exterior-of-musee-du-louvre-museum-paris-france-bernard-jaubert.jpg\", lm2tag, \"Exterior of the Louvre museum\")\nlm2body = BoldText(\"text\", \"The Louvre\", \"World's largest art museum\")\nlm2tag = Tag(\"8.9M visits/yr\", lm2tagIcon, \"sm\", \"success\")\nlm2tagIcon = Icon(\"trending-up\", \"charts\")\nlm3 = VisualCardItem(lm3body, \"notre-dame\", \"https://cdn.britannica.com/29/255529-050-63A22A3C/notre-dame-de-paris-cathedral-paris-france.jpg\", lm3tag, \"Notre-Dame cathedral facade\")\nlm3body = BoldText(\"text\", \"Notre-Dame\", \"Fully restored cathedral\")\nlm3tag = Tag(\"Reopened 2024\", lm3tagIcon, \"sm\", \"warning\")\nlm3tagIcon = Icon(\"hammer\", \"tools\")\nlm4 = VisualCardItem(lm4body, \"versailles\", \"https://cdn.britannica.com/76/130076-050-31128A3D/Latona-Fountain-Balthazard-Marsy-Gaspard-Andre-Le.jpg\", lm4tag, \"Latona Fountain at Versailles\")\nlm4body = BoldText(\"text\", \"Palace of Versailles\", \"Royal gardens & grand halls\")\nlm4tag = Tag(\"Day Trip\", lm4tagIcon, \"sm\", \"neutral\")\nlm4tagIcon = Icon(\"train-front\", \"travel\")\nsecFood = SectionItem(\"food\", \"Food & Drink\", [foodCards])\nfoodCards = ContextCardBlock([f1, f2, f3], \"grid\", true, { type: \"continue_conversation\", context: \"Tell me more about eating and drinking in Paris\" })\nf1 = ContextCardItem(\"bistros\", \"Bistros\", \"Classic French fare — try Les Arlots for confit beef cheeks.\", \"gray\", \"https://everydayparisian.com/wp-content/uploads/2023/01/IMG_0221-768x1024.webp\")\nf2 = ContextCardItem(\"fine-dining\", \"Fine Dining\", \"Septime leads a new wave of creative Michelin-starred cuisine.\", \"gray\", \"https://production-data.worldofmouth.app/images/72b2e6db-97ea-49c4-aa5d-3b7c8121c812.jpg\")\nf3 = ContextCardItem(\"bakeries\", \"Bakeries\", \"Shinya Pain for sourdough. Grand Prix-winning baguettes citywide.\", \"gray\", \"https://www.davidlebovitz.com/wp-content/uploads/2018/09/Le-petit-grain-paris-bakery-boulangerie-patisserie-pastry-shop-8-640x895.jpg\")\nsecWhen = SectionItem(\"when\", \"Best Time to Visit\", [whenTabs])\nwhenTabs = Tabs([tabSpring, tabSummer])\ntabSpring = TabItem(\"spring\", \"Spring\", [springList])\nspringList = ListBlock([sp1, sp2, sp3], \"number\")\nsp1 = ListItem(\"Cherry blossoms at Parc de Sceaux\", \"March – May\", null, null, { type: \"continue_conversation\", context: \"Tell me about cherry blossom season in Paris\" })\nsp2 = ListItem(\"Picnics along the Seine & in city parks\", \"Warm, lively atmosphere\", null, null, { type: \"continue_conversation\", context: \"Where are the best picnic spots in Paris?\" })\nsp3 = ListItem(\"Fewer crowds than summer\", \"Ideal for sightseeing\", null, null, { type: \"continue_conversation\", context: \"How busy is Paris in spring?\" })\ntabSummer = TabItem(\"summer\", \"Summer\", [summerList])\nsummerList = ListBlock([su1, su2, su3], \"number\")\nsu1 = ListItem(\"Swim in the Seine — open July & August 2026\", \"First time in 102 years\", null, null, { type: \"continue_conversation\", context: \"Tell me about swimming in the Seine\" })\nsu2 = ListItem(\"Open-air cinema at Parc de la Villette\", \"Free films under the stars\", null, null, { type: \"continue_conversation\", context: \"Tell me about the open-air cinema at Parc de la Villette\" })\nsu3 = ListItem(\"Bastille Day celebrations on July 14\", \"Fireworks at the Eiffel Tower\", null, null, { type: \"continue_conversation\", context: \"What happens in Paris on Bastille Day?\" })\nactionBtns = Buttons([btn1, btn2, btn3], \"row\")\nbtn1 = Button(\"Plan a Trip to Paris\", { type: \"continue_conversation\", context: \"Help me plan a trip to Paris\" }, \"primary\")\nbtn2 = Button(\"Best Hotels in Paris\", { type: \"continue_conversation\", context: \"Show me the best hotels in Paris\" }, \"secondary\")\nbtn3 = Button(\"Build a Paris Itinerary\", { type: \"continue_conversation\", context: \"Build a 5-day Paris itinerary for me\" }, \"secondary\")\nsrc1 = { title: \"Paris Travel Guide 2025\", sourceName: \"Travel and Tour World\", url: \"https://www.travelandtourworld.com/news/article/from-iconic-landmarks-to-hidden-gems-the-ultimate-paris-travel-guide-for-2025-you-need-to-read-now/\" }\nsrc2 = { title: \"Best Things to Do in Paris 2026\", sourceName: \"Time Out Paris\", url: \"http://www.timeout.fr/paris/en/for-tourists\" }", + "Example 2 — Data and analytics response (quantitative asks lead with KPIs and charts):\n\nroot = Card([pageHeader, kpiBlock, sections, deepDiveHeader, compositeCards, actionButtons])\npageHeader = CardHeader(\"Analytics Dashboard\", \"April 2026 · Business Performance Overview\")\nkpiBlock = SnippetCardBlock([kpi1, kpi2, kpi3, kpi4], \"grid\", true)\nkpi1 = SnippetCardItem(\"total-revenue\", kpi1lhs, kpi1rhs)\nkpi1lhs = IconText(kpi1icon, \"neutral\", \"m\", \"Total Revenue\", \"Month to date\", false, \"horizontal\")\nkpi1icon = Icon(\"circle-dollar-sign\", \"finance\")\nkpi1rhs = BoldText(\"number\", \"$284,500\", \"+12.4%\", \"metric\")\nkpi2 = SnippetCardItem(\"active-users\", kpi2lhs, kpi2rhs)\nkpi2lhs = IconText(kpi2icon, \"neutral\", \"m\", \"Active Users\", \"Monthly active users\", false, \"horizontal\")\nkpi2icon = Icon(\"users\", \"people\")\nkpi2rhs = BoldText(\"number\", \"47,320\", \"+6.8%\", \"metric\")\nkpi3 = SnippetCardItem(\"conversion-rate\", kpi3lhs, kpi3rhs)\nkpi3lhs = IconText(kpi3icon, \"neutral\", \"m\", \"Conversion Rate\", \"Avg. this month\", false, \"horizontal\")\nkpi3icon = Icon(\"trending-up\", \"charts\")\nkpi3rhs = BoldText(\"number\", \"5.7%\", \"+0.9%\", \"metric\")\nkpi4 = SnippetCardItem(\"churn-rate\", kpi4lhs, kpi4rhs)\nkpi4lhs = IconText(kpi4icon, \"neutral\", \"m\", \"Churn Rate\", \"Monthly customer churn\", false, \"horizontal\")\nkpi4icon = Icon(\"user-minus\", \"people\")\nkpi4rhs = BoldText(\"number\", \"2.1%\", \"-0.4%\", \"metric\")\nsections = SectionBlock([secUsers, secChannels], false)\nsecUsers = SectionItem(\"users\", \"User Activity\", [userTabs])\nuserTabs = Tabs([tabGrowth, tabRetention])\ntabGrowth = TabItem(\"growth\", \"User Growth\", [growthChart])\ngrowthChart = LineChart(months, [dauSeries, mauSeries], \"natural\", \"Month\", \"Users (k)\")\nmonths = [\"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\", \"Jan\", \"Feb\", \"Mar\", \"Apr\"]\ndauSeries = Series(\"DAU\", [5.1, 5.4, 5.8, 6.2, 6.7, 7.1, 7.6, 8.0, 8.4, 8.9, 9.3, 9.8])\nmauSeries = Series(\"MAU\", [28.4, 30.1, 31.8, 33.5, 35.2, 37.1, 38.8, 40.4, 42.1, 43.9, 45.6, 47.3])\ntabRetention = TabItem(\"retention\", \"Cohort Retention\", [retentionChart])\nretentionChart = LineChart(cohortMonths, [cohortJan, cohortFeb, cohortMar], \"natural\", \"Month Since Signup\", \"Retention (%)\")\ncohortMonths = [\"Month 0\", \"Month 1\", \"Month 2\", \"Month 3\", \"Month 4\", \"Month 5\", \"Month 6\"]\ncohortJan = Series(\"Jan Cohort\", [100, 80, 70, 62, 56, 51, 47])\ncohortFeb = Series(\"Feb Cohort\", [100, 83, 73, 65, 59, 54, 50])\ncohortMar = Series(\"Mar Cohort\", [100, 86, 76, 68, 62, 0, 0])\nsecChannels = SectionItem(\"channels\", \"Acquisition Channels\", [channelTabs])\nchannelTabs = Tabs([tabChannelBar, tabChannelTable])\ntabChannelBar = TabItem(\"channel-bar\", \"Revenue by Channel\", [channelBarChart])\nchannelBarChart = HorizontalBarChart(channels, [channelSeries], \"grouped\", \"Revenue ($k)\", \"Channel\")\nchannels = [\"Organic Search\", \"Paid Ads\", \"Referral Program\", \"Partner Network\", \"Direct / Brand\", \"Email Campaigns\"]\nchannelSeries = Series(\"Revenue ($k)\", [82.4, 68.1, 54.6, 38.2, 26.8, 14.4])\ntabChannelTable = TabItem(\"channel-table\", \"Full Breakdown\", [channelTable])\nchannelTable = Table([colChannel, colLeads, colConv, colCac, colRevenue, colStatus])\ncolChannel = Col(\"Channel\", channels, \"string\")\ncolLeads = Col(\"Leads\", [4210, 3180, 2050, 1120, 1290, 820], \"number\")\ncolConv = Col(\"Conv. Rate\", [\"6.4%\", \"5.1%\", \"9.4%\", \"11.8%\", \"5.8%\", \"8.1%\"], \"string\")\ncolCac = Col(\"CAC ($)\", [188, 395, 132, 109, 218, 154], \"number\")\ncolRevenue = Col(\"Revenue ($k)\", [82.4, 68.1, 54.6, 38.2, 26.8, 14.4], \"number\")\ncolStatus = Col(\"Status\", [\"Scaling\", \"Optimizing\", \"Scaling\", \"Growing\", \"Stable\", \"Stable\"], \"string\")\ndeepDiveHeader = InlineHeader(\"Support Health & Marketing ROI\", \"Click a card to explore deeper insights\")\ncompositeCards = CompositeCardBlock([cardSupport, cardMarketing], \"grid\", true, { type: \"continue_conversation\", context: \"Explore this deep-dive area in more detail\" })\ncardSupport = CompositeCardItem(\"support-health\", supportHeader, [supportMetric, supportChart, supportList])\nsupportHeader = IconText(supportIcon, \"neutral\", \"m\", \"Support Health\", \"Ticket volume & resolution trends\", false, \"horizontal\")\nsupportIcon = Icon(\"headphones\", \"communication\")\nsupportMetric = MetricIndicatorInline(\"94.2% CSAT\", \"Customer Satisfaction Score\", { direction: \"up\", value: 2.1 })\nsupportChart = BarChart([\"Jan\", \"Feb\", \"Mar\", \"Apr\"], [openSeries, resolvedSeries], \"grouped\", \"Month\", \"Tickets\")\nopenSeries = Series(\"Opened\", [341, 327, 308, 289])\nresolvedSeries = Series(\"Resolved\", [319, 318, 301, 283])\nsupportList = EntityList([sup1, sup2, sup3], \"small\")\nsup1 = { left: \"Avg. Resolution Time\", right: \"4h 12m\", rightVariant: \"text\" }\nsup2 = { left: \"First Contact Resolution\", right: \"78.4%\", rightVariant: \"number\" }\nsup3 = { left: \"Escalation Rate\", right: \"6.2%\", rightVariant: \"number\" }\ncardMarketing = CompositeCardItem(\"marketing-roi\", marketingHeader, [marketingMetric, marketingChart, marketingList])\nmarketingHeader = IconText(marketingIcon, \"neutral\", \"m\", \"Marketing ROI\", \"Spend vs. pipeline generated\", false, \"horizontal\")\nmarketingIcon = Icon(\"megaphone\", \"communication\")\nmarketingMetric = MetricIndicatorInline(\"3.8x Blended ROI\", \"Return on Ad Spend\", { direction: \"up\", value: 0.4 })\nmarketingChart = AreaChart([\"Jan\", \"Feb\", \"Mar\", \"Apr\"], [spendSeries, pipelineSeries], \"natural\", \"Month\", \"Value ($k)\")\nspendSeries = Series(\"Ad Spend\", [44.0, 46.8, 49.2, 51.6])\npipelineSeries = Series(\"Pipeline Generated\", [158.2, 171.5, 184.9, 196.1])\nmarketingList = EntityList([mkt1, mkt2, mkt3], \"small\")\nmkt1 = { left: \"Total Ad Spend (Apr)\", right: \"$51,600\", rightVariant: \"number\" }\nmkt2 = { left: \"Cost per Lead\", right: \"$48.20\", rightVariant: \"number\" }\nmkt3 = { left: \"Top Channel\", right: \"Organic Search\", rightVariant: \"text\" }\nactionButtons = Buttons([btnChannels, btnForecast], \"row\")\nbtnChannels = Button(\"Break Down by Channel\", { type: \"continue_conversation\", context: \"Break down this month's revenue by acquisition channel\" }, \"primary\")\nbtnForecast = Button(\"Forecast Next Quarter\", { type: \"continue_conversation\", context: \"Forecast revenue and active users for next quarter\" }, \"secondary\")", + "Example 3 — Form response (help-me-choose/plan asks: inspiration cards FIRST, then a compact form). The inspiration-card images below came from an image tool call — include such cards ONLY when you have real URLs; with no real URLs, start directly with the form:\n\nroot = Card([header, intro, inspoCards, form])\nheader = CardHeader(\"Plan Your Trip\", \"A few ideas to spark inspiration — then tell me your preferences\")\nintro = TextContent(\"Tap a style to jump straight in, or fill the quick form below for a tailored plan.\")\ninspoCards = VisualCardBlock([insp1, insp2], \"grid\", true, { type: \"continue_conversation\", context: \"Plan me a trip in this style with destinations, itinerary, and costs\" })\ninsp1 = VisualCardItem(insp1body, \"inspo-beach\", \"https://www.travelandleisure.com/thmb/cuAB7XMQ6s_Gji-no956KNopT7M=/1500x0/filters:no_upscale():max_bytes(150000):strip_icc()/TAL-maldives-TROPVACAY0325-1b5047d5931d4850b47d082966f9f563.jpg\", insp1tag, \"Maldives overwater villas\")\ninsp1body = BoldText(\"text\", \"Tropical Beach Escape\", \"Overwater villas & snorkeling\")\ninsp1tag = Tag(\"Relaxation\", insp1icon, \"sm\", \"info\")\ninsp1icon = Icon(\"umbrella\", \"travel\")\ninsp2 = VisualCardItem(insp2body, \"inspo-mountain\", \"https://www.salkantaytrekmachu.com/img/adventure-travel-peru-032.jpg\", insp2tag, \"Trekking in the Peruvian Andes\")\ninsp2body = BoldText(\"text\", \"Mountain Adventure\", \"Trekking & epic views\")\ninsp2tag = Tag(\"Active\", insp2icon, \"sm\", \"warning\")\ninsp2icon = Icon(\"mountain\", \"nature\")\nform = Form(\"trip-planner\", formButtons, [fcDestination, fcDates, fcTravellers, fcTripType, fcBudget, fcStay, fcNotes])\nformButtons = Buttons([btnSubmit, btnSkip], \"row\")\nbtnSubmit = Button(\"Plan My Trip\", { type: \"continue_conversation\", context: \"Help me plan a trip based on my form submission\" }, \"primary\")\nbtnSkip = Button(\"Just Surprise Me\", { type: \"continue_conversation\", context: \"Skip the form and surprise me with a destination and itinerary\" }, \"secondary\")\nfcDestination = FormControl(\"Destination\", destinationInput, \"Leave blank if you'd like suggestions\")\ndestinationInput = Input(\"destination\", \"e.g. Tokyo, Japan — or leave blank\", \"text\")\nfcDates = FormControl(\"Travel Dates\", datesPicker, \"Select your start and end dates\")\ndatesPicker = DatePicker(\"travel-dates\", \"range\", { required: true })\nfcTravellers = FormControl(\"Number of Travellers\", travellersSelect, \"How many people are travelling?\")\ntravellersSelect = Select(\"num-travellers\", [tr1, tr2, tr3, tr4], \"Select number of travellers\", { required: true })\ntr1 = SelectItem(\"1\", \"1 — Solo\")\ntr2 = SelectItem(\"2\", \"2 — Couple\")\ntr3 = SelectItem(\"3-4\", \"3–4 People\")\ntr4 = SelectItem(\"5+\", \"5+ People\")\nfcTripType = FormControl(\"Trip Type\", tripTypeCards, \"What kind of trip are you planning?\")\ntripTypeCards = OptionCards(\"trip-type\", \"single\", [oc1, oc2, oc3, oc4], { required: true })\noc1 = OptionCard(\"adventure\", \"Adventure\", \"Hiking, trekking & outdoor activities\", oc1icon)\noc1icon = Icon(\"mountain\", \"nature\")\noc2 = OptionCard(\"beach\", \"Beach & Relaxation\", \"Sun, sand & leisure\", oc2icon)\noc2icon = Icon(\"waves\", \"nature\")\noc3 = OptionCard(\"cultural\", \"Cultural & Historical\", \"Museums, heritage & local experiences\", oc3icon)\noc3icon = Icon(\"landmark\", \"buildings\")\noc4 = OptionCard(\"city\", \"City Break\", \"Urban exploration & nightlife\", oc4icon)\noc4icon = Icon(\"building-2\", \"buildings\")\nfcBudget = FormControl(\"Estimated Budget (per person)\", budgetSlider, \"Drag to set your per-person budget in USD\")\nbudgetSlider = Slider(\"budget\", \"discrete\", 500, 10000, 500, [2500], \"Budget (USD)\")\nfcStay = FormControl(\"Accommodation Preference\", stayChips, \"Select all that apply\")\nstayChips = Chips(\"accommodation\", \"multiple\", [ch1, ch2, ch3, ch4], {}, [\"hotel\"])\nch1 = ChipItem(\"hotel\", \"Hotel\", ch1icon)\nch1icon = Icon(\"building\", \"buildings\")\nch2 = ChipItem(\"resort\", \"Resort\", ch2icon)\nch2icon = Icon(\"umbrella\", \"travel\")\nch3 = ChipItem(\"airbnb\", \"Vacation Rental\", ch3icon)\nch3icon = Icon(\"home\", \"home\")\nch4 = ChipItem(\"camping\", \"Camping / Glamping\", ch4icon)\nch4icon = Icon(\"tent\", \"nature\")\nfcNotes = FormControl(\"Special Requests or Notes\", notesArea, \"Any dietary needs, accessibility requirements, or specific interests?\")\nnotesArea = TextArea(\"special-requests\", \"e.g. vegetarian meals, wheelchair access, honeymoon surprise...\", 4)", + "Example 4 — Recommendations with images and links (same pattern for products, movies, restaurants, articles):\n\nroot = Card([header, intro, posters, linksHeader, trackCards], [src1, src2])\nheader = CardHeader(\"Retro-Energetic Indian Tracks\", \"New Hindi releases with 80s/90s dance DNA\")\nintro = TextContent(\"These recent tracks channel retro synths and 90s party percussion [1][2]. Tap a poster to explore a track, or use its button to watch the exact official video.\")\nposters = VisualCardBlock([v1, v2], \"grid\", true, { type: \"continue_conversation\", context: \"Tell me more about this track and its retro influences\" })\nv1 = VisualCardItem(v1body, \"tamma\", \"https://i.ytimg.com/vi/PQHeOb0Q9oo/hq720.jpg\", v1tag, \"Tamma Tamma video poster\")\nv1body = BoldText(\"text\", \"Tamma Tamma\", \"Dhurandhar · Ranveer Singh\")\nv1tag = Tag(\"80s Electro-Disco\", v1icon, \"sm\", \"success\")\nv1icon = Icon(\"disc-3\", \"multimedia\")\nv2 = VisualCardItem(v2body, \"sajan\", \"https://i.ytimg.com/vi/mLz-qBiRnl8/hq720.jpg\", v2tag, \"Sajan Re video poster\")\nv2body = BoldText(\"text\", \"Sajan Re\", \"Nora Fatehi · Badshah\")\nv2tag = Tag(\"Synthwave Electro\", v2icon, \"sm\", \"info\")\nv2icon = Icon(\"music\", \"multimedia\")\nlinksHeader = InlineHeader(\"Watch the Exact Videos\", \"Each button opens that track's official video\")\ntrackCards = CompositeCardBlock([t1, t2], \"grid\", true)\nt1 = CompositeCardItem(\"t-tamma\", t1h, [t1body, t1tags], { button: t1btn })\nt1h = IconText(t1icon, \"neutral\", \"m\", \"Tamma Tamma\", \"Bappi Lahiri classic, electro-funk rework\", true, \"horizontal\")\nt1icon = Icon(\"disc-3\", \"multimedia\")\nt1body = Text(\"text\", \"Heavy analog synths and brass over a fast disco groove.\", \"Retro Electro-Disco\")\nt1tags = TagBlock([\"2026 Film Hit\", \"Peak Party\"])\nt1btn = Button(\"Watch on YouTube\", { type: \"open_url\", url: \"https://www.youtube.com/watch?v=PQHeOb0Q9oo\" }, \"primary\")\nt2 = CompositeCardItem(\"t-sajan\", t2h, [t2body, t2tags], { button: t2btn })\nt2h = IconText(t2icon, \"neutral\", \"m\", \"Sajan Re\", \"Nora Fatehi · Badshah · Sanjoy\", true, \"horizontal\")\nt2icon = Icon(\"music\", \"multimedia\")\nt2body = Text(\"text\", \"Synthwave melody hooks over a modern club beat.\", \"Retro-Fusion Electro\")\nt2tags = TagBlock([\"Club Anthem\", \"Nora x Badshah\"])\nt2btn = Button(\"Watch on YouTube\", { type: \"open_url\", url: \"https://www.youtube.com/watch?v=COUSCnFazzc\" }, \"primary\")\nsrc1 = { title: \"Tamma Tamma (Full Video) - Dhurandhar\", sourceName: \"YouTube (T-Series)\", url: \"https://www.youtube.com/watch?v=PQHeOb0Q9oo\" }\nsrc2 = { title: \"Sajan Re (Official Music Video)\", sourceName: \"YouTube (T-Series)\", url: \"https://www.youtube.com/watch?v=COUSCnFazzc\" }", + "Example 5 — Table with follow-ups:\n\nroot = Card([title, tbl, followUps])\ntitle = TextContent(\"Top Languages\", \"large-heavy\")\ntbl = Table([Col(\"Language\", langs), Col(\"Users (M)\", users), Col(\"Year\", years)])\nlangs = [\"Python\", \"JavaScript\", \"Java\"]\nusers = [15.7, 14.2, 12.1]\nyears = [1991, 1995, 1995]\nfollowUps = FollowUpBlock([fu1, fu2])\nfu1 = FollowUpItem(\"Tell me more about Python\")\nfu2 = FollowUpItem(\"Show me a JavaScript comparison\")", + ] + + /// Extra prompt rules for this library (`openuiChatAdditionalRules`). + public static let additionalRules: [String] = [ + "When asked about data, generate realistic/plausible data", + "Every response is a single Card(children) — children stack vertically automatically. No layout params are needed on Card.", + "Card is the only layout container. Do NOT use Stack. Use Tabs to switch between sections, Carousel for horizontal scroll.", + "Use FollowUpBlock at the END of a Card to suggest what the user can do or ask next.", + "Use ListBlock when presenting a set of options or steps; give each ListItem an action (5th argument) to make it clickable — without one the item is plain text.", + "Use SectionBlock to group long responses into collapsible sections — good for reports, FAQs, and structured content.", + "Use SectionItem inside SectionBlock: each item needs a unique value id, a trigger (header label), and a content array.", + "Carousel takes an array of slides, where each slide is an array of content: carousel = Carousel([[t1, img1], [t2, img2]])", + "IMPORTANT: Every slide in a Carousel must use the same component structure in the same order — e.g. all slides: [title, image, description, tags].", + "For forms, define one FormControl reference per field so controls can stream progressively.", + "For forms, always provide the second Form argument with Buttons(...) actions: Form(name, buttons, fields).", + "Never nest Form inside Form.", + "CardHeader is the page-level heading — place it as the first child of Card; InlineHeader is a lighter subsection label for a block inside the response, and is redundant when the following component already has a heading.", + "Never use Callout or TextCallout to justify why you did what you did or for any meta-information.", + "Every image slot (Image, ImageBlock, ImageGallery, VisualCardItem/ContextCardItem bgImageSrc, ImageText, ImageTextLarge, ListItem image) requires a REAL URL from a tool result, the conversation, or the user — NEVER invent, guess, or template a URL.", + "If no image URL is in context, compose without image components — icons, gray ContextCardItems, and charts still make rich layouts.", + "Argument-order trap: Image takes alt FIRST, while ImageBlock, ImageText, and ImageTextLarge take src FIRST — alt text in a src slot ships a broken image.", + "Card blocks (SnippetCardBlock, OverviewCardBlock, ContextCardBlock, CompositeCardBlock, VisualCardBlock) need at least 2 items, all with the same structure; use layout \"grid\" for primary side-by-side comparison and \"carousel\" for secondary browsing.", + "The card-block action is BLOCK-LEVEL: one action shared by every card, with the clicked card's details attached automatically — keep continue_conversation context generic (e.g. \"Tell me more about this destination\"), and never use a block-level open_url since it would send every card to the same URL.", + "Card items have NO per-item action: to give each item its own external link, use CompositeCardItem with footer { button: Button(label, { type: \"open_url\", url }) } — never claim a card opens a link it cannot.", + "Text, BoldText, IconText, ImageText, ImageTextLarge and MetricIndicator* are inline building blocks used INSIDE card items — do not place them directly in the root Card.", + "EntityList size \"small\" is reserved for CompositeCardItem body content (no header/footer); use size \"default\" elsewhere.", + "Tag variant must be semantically correct (success=positive, danger=negative, warning=cautionary, info=informational, neutral=label); Tag takes an optional Icon second, so use Tag(text, icon, size, variant) when a variant is needed.", + "TagBlock takes plain strings — keep it to at most 3 tags.", + "ListBlock variant \"number\" is for ordered sequences and rankings, \"image\" for products/profiles with a real image; Steps is for ordered sequential processes only.", + "Use SectionBlock to divide a long response into independent, equally prominent topics; use Tabs when the items are alternative views of the same thing; set isFoldable=false unless a section has 4+ distinct blocks; never make SectionBlock the first Card child.", + "Accordion is for supplementary or FAQ content only — never primary information, 3-5 items max, and always the last component in its section or card.", + "Charts: LineChart/AreaChart variant \"natural\" for smooth trends and \"linear\" for precise point-to-point data; BarChart \"grouped\" for side-by-side comparison and \"stacked\" for part-to-whole; prefer HorizontalBarChart when category labels are long or numerous.", + "For large chart values, scale the data and put the unit in the axis label (e.g. yLabel \"Revenue ($k)\") instead of passing raw thousands or millions.", + "Quantitative asks get a chart (plus KPI cards where useful) in the FIRST response — do not answer numbers with prose or a table alone.", + "Table is column-oriented: each Col holds its own data array of plain strings or numbers (never components), and every column's array must have the same length.", + "Use EditableTable ONLY when the user or app explicitly asks for an editable table; default to Table for read-only data.", + "Every Form MUST have EXACTLY ONE submit Button with variant \"primary\" — that button validates the form; other buttons (secondary/tertiary escape hatches like \"Just Surprise Me\") skip validation, and there is never a reset button.", + "Prefer structured inputs (Select, Chips, OptionCards, DatePicker, Slider) over free-text Input, give every FormControl a clear label (and a hint where it prevents mistakes), and keep each field name unique within the response.", + "Validation rules ({ required: true, email: true, minLength: 8, ... }) are enforced with inline errors — use them for genuinely essential fields, but never require a free-text field where leaving it blank is meaningful.", + "Form inputs work ONLY inside a FormControl within a Form; for tappable suggestion choices OUTSIDE a form, use Buttons of continue_conversation Buttons — never a bare OptionCards or Chips.", + "Only add Buttons for real actions: form submit, open_url with a URL from a tool or the user (never fabricated), or a continue_conversation follow-up — never a button for an app action (open, download, export, edit) you cannot perform.", + "Every Button label must accurately describe what clicking it does; use \"primary\" for the main action and \"secondary\"/\"tertiary\" only for lower-emphasis supporting actions.", + "Icon: always include the category argument — it enables fallback matching when the exact lucide name is unavailable.", + "Pass sources on Card ONLY when the answer relies on real references you actually have, and cite them inline in TextContent as [1], [2] (1-based index into sources).", + ] + + /// The prompt options react-ui pairs with this library (`openuiChatPromptOptions`). + public static let promptOptions = PromptOptions( + additionalRules: additionalRules, examples: examples) +} diff --git a/packages/swift-lang/Sources/OpenUILang/CloudConfig.swift b/packages/swift-lang/Sources/OpenUILang/CloudConfig.swift new file mode 100644 index 0000000000..e553fe3e89 --- /dev/null +++ b/packages/swift-lang/Sources/OpenUILang/CloudConfig.swift @@ -0,0 +1,187 @@ +/// OpenUI Cloud's managed system prompt. Port of lang-core's +/// `parser/cloud-config.ts` and `parser/validate-library.ts`. + +/// The config block header. The trailing newline is part of the wire contract. +private let cloudConfigMarker = "]]>openui:config\n" + +/// The version of Cloud's built-in chat library requested when no library is +/// given. Cloud rejects a non-numeric or too-old version. +private let cloudChatLibraryVersion = "0.1.0" + +/// A library OpenUI Cloud can't use, or prompt options it can't apply. +public struct CloudConfigError: Error, CustomStringConvertible { + public var description: String +} + +/// OpenUI Cloud's config block: the Swift version of +/// `generateSystemPrompt({ cloud: true, library, promptOptions, instructions })`. +/// +/// Send it as the system message and Cloud builds the full prompt on its side. +/// Without a library, Cloud uses its built-in chat library. Only `preamble`, +/// `additionalRules` and `examples` are sent; Cloud ignores the other options. +/// +/// - Parameter library: A `LibrarySpec`, as returned by `Library.toSpec()`. +/// - Throws: ``CloudConfigError`` when the library is malformed, or when +/// prompt options are given without a library. +public func generateCloudConfig( + library: OpenUIValue? = nil, promptOptions: PromptOptions? = nil, instructions: String? = nil +) throws -> String { + var config = OpenUIObject() + if let library, library.isTruthy { + let issues = validateChatLibrary(library) + if !issues.isEmpty { + throw CloudConfigError( + description: + "[generateSystemPrompt] Invalid library: \(issues.joined(separator: " "))") + } + var chatLibrary = library.objectValue ?? OpenUIObject() + chatLibrary["components"] = nil + config["chatLibrary"] = .object(chatLibrary) + if let options = promptOptions.flatMap(cloudPromptOptions) { + config["systemPromptOptions"] = .object(options) + } + } else { + if promptOptions.flatMap(cloudPromptOptions) != nil { + throw CloudConfigError( + description: + "[generateSystemPrompt] promptOptions requires a library — the built-in library ignores it." + ) + } + config["libraryVersion"] = .string(cloudChatLibraryVersion) + } + + let block = cloudConfigMarker + JSON.stringify(.object(config)) + if let instructions, !instructions.isEmpty { return "\(block)\n\(instructions)" } + return block +} + +extension Library { + /// This library's OpenUI Cloud config block. See ``generateCloudConfig(library:promptOptions:instructions:)``. + public func cloudConfig(_ options: PromptOptions? = nil, instructions: String? = nil) throws + -> String + { + try generateCloudConfig(library: toSpec(), promptOptions: options, instructions: instructions) + } +} + +/// The options Cloud's prompt assembler reads, in lang-core's key order, or +/// nil when none are set. An empty `preamble` counts as unset, like in JavaScript. +private func cloudPromptOptions(_ options: PromptOptions) -> OpenUIObject? { + var picked = OpenUIObject() + if let examples = options.examples { picked["examples"] = .array(examples.map { .string($0) }) } + if let preamble = options.preamble, !preamble.isEmpty { picked["preamble"] = .string(preamble) } + if let rules = options.additionalRules { + picked["additionalRules"] = .array(rules.map { .string($0) }) + } + return picked.isEmpty ? nil : picked +} + +// MARK: - Library validation + +/// Structural checks Cloud applies to a library, reporting every issue in one +/// pass. lang-core's issues also carry a code and a path, but only their +/// messages are ever read, so these are the messages. +func validateChatLibrary(_ library: OpenUIValue) -> [String] { + guard let spec = library.objectValue else { + return ["chatLibrary must be an object."] + } + var issues: [String] = [] + + let root = spec["root"] ?? .undefined + let rootName = root.stringValue.flatMap { $0.isEmpty ? nil : $0 } + if root != .undefined, rootName == nil { + issues.append("chatLibrary.root, when present, must be a non-empty string.") + } + + guard let schema = spec["schema"]?.objectValue else { + issues.append("chatLibrary.schema must be an object with a $defs map of component schemas.") + return issues + } + guard let defs = schema["$defs"]?.objectValue, !defs.isEmpty else { + issues.append("chatLibrary.schema.$defs must be a non-empty object keyed by component name.") + return issues + } + let defNames = Set(defs.keys) + + if let rootName, !defNames.contains(rootName) { + issues.append( + "Root component \"\(rootName)\" was not found in schema.$defs. Available components: \(defs.keys.joined(separator: ", "))." + ) + } + + for (name, def) in defs.entries { + let defPath = "$defs/\(name)" + guard let component = def.objectValue else { + issues.append("\(defPath) must be an object component schema.") + continue + } + + let properties = component["properties"] ?? .undefined + if properties != .undefined, properties.objectValue == nil { + issues.append("\(defPath)/properties must be an object.") + } + + let required = component["required"] ?? .undefined + if required != .undefined { + let names = required.arrayValue?.map(\.stringValue) + if let names, names.allSatisfy({ $0 != nil }) { + let propKeys = Set(properties.objectValue?.keys ?? []) + for case let name? in names where !propKeys.contains(name) { + issues.append("\(defPath) lists required property \"\(name)\" that is not in properties.") + } + } else { + issues.append("\(defPath)/required must be an array of property names.") + } + } + + collectRefIssues(properties, "\(defPath)/properties", defNames, &issues) + } + + for group in spec["componentGroups"]?.arrayValue ?? [] { + guard let groupName = group["name"].stringValue, let components = group["components"].arrayValue + else { + issues.append( + "Each componentGroups entry must be {name: string, components: string[], notes?: string[]}." + ) + continue + } + for component in components + where !(component.stringValue.map { defNames.contains($0) } ?? false) { + issues.append( + "Component group \"\(groupName)\" references unknown component \"\(component.jsString)\".") + } + } + + return issues +} + +/// Checks every `$ref` below `node` resolves to a component in `$defs`. +private func collectRefIssues( + _ node: OpenUIValue, _ path: String, _ defNames: Set, _ issues: inout [String] +) { + if let items = node.arrayValue { + for (i, item) in items.enumerated() { + collectRefIssues(item, "\(path)/\(i)", defNames, &issues) + } + return + } + guard let object = node.objectValue else { return } + + if let ref = object["$ref"]?.stringValue, !refResolves(ref, defNames) { + issues.append( + "Unresolvable $ref \"\(ref)\" at \(path) — refs must be \"#/$defs/\" pointing at a component in $defs." + ) + } + for (key, value) in object.entries where key != "$ref" { + collectRefIssues(value, "\(path)/\(key)", defNames, &issues) + } +} + +/// `/^#\/\$defs\/(.+)$/` plus a `$defs` lookup. `.` doesn't match line terminators. +private func refResolves(_ ref: String, _ defNames: Set) -> Bool { + let prefix = "#/$defs/" + guard ref.hasPrefix(prefix) else { return false } + let name = String(ref.dropFirst(prefix.count)) + let lineTerminators: Set = ["\n", "\r", "\r\n", "\u{2028}", "\u{2029}"] + return !name.isEmpty && !name.contains(where: lineTerminators.contains) && defNames.contains(name) +} diff --git a/packages/swift-lang/Sources/OpenUILang/CloudMessages.swift b/packages/swift-lang/Sources/OpenUILang/CloudMessages.swift new file mode 100644 index 0000000000..98f382e53f --- /dev/null +++ b/packages/swift-lang/Sources/OpenUILang/CloudMessages.swift @@ -0,0 +1,306 @@ +/// OpenUI Cloud's inline message format: the markers that separate a message's +/// openui-lang content from the context it carries (form state, what the user +/// clicked). Port of react-ui's `utils/sentinelParser.ts`, plus the messages +/// react-ui's `GenUIAssistantMessage` builds with it. +/// +/// Like the JavaScript, this works on UTF-16 code units, so `\r\n` is two +/// units rather than one Swift `Character`. + +private let inlineSentinel = "]]>openui:" +private let contentMarker = Array("\(inlineSentinel)content".utf16) +private let contextMarker = Array("\(inlineSentinel)context".utf16) +private let endMarker = Array("\(inlineSentinel)end".utf16) +private let artifactPrefix = Array("\(inlineSentinel)artifact ".utf16) + +/// Tokens whose partial prefix can sit at the tail of a still-streaming chunk. +/// Trimming keeps a half-arrived marker from flashing as raw text; the next +/// delta re-parses the whole message, so a false positive is transient. +private let streamPartialTokens = [contentMarker, contextMarker, endMarker] + +private let newline = UInt16(ascii: "\n") +private let carriageReturn = UInt16(ascii: "\r") + +/// A message split into its sections. +public struct ParsedMessageContent: Sendable, Equatable { + /// The openui-lang program (or plain text) to render. + public var content: String + /// The context section's JSON, if there is one. + public var contextString: String? + /// The content marker line, attributes included, so it can be written back + /// unchanged when the message is saved again. + public var contentHeader: String? + /// Whether the `]]>openui:end` marker was present: the stream reached its + /// last chunk. A saved message without it was cut off mid-stream. + public var end: Bool + + public init( + content: String, contextString: String? = nil, contentHeader: String? = nil, end: Bool = false + ) { + self.content = content + self.contextString = contextString + self.contentHeader = contentHeader + self.end = end + } +} + +/// `text` as a content section. +public func wrapContent(_ text: String) -> String { + "\(inlineSentinel)content\n\(text)" +} + +/// `text` as a content section under its original header line, so the +/// header's attributes survive saving form state into the message. +public func wrapContentWithHeader(_ text: String, _ contentHeader: String?) -> String { + if let contentHeader, !contentHeader.isEmpty { return "\(contentHeader)\n\(text)" } + return wrapContent(text) +} + +/// `json` as a context section, to append after the content. +public func wrapContext(_ json: String) -> String { + "\n\(inlineSentinel)context\n\(json)" +} + +/// Separates a message's content from its context, dropping what is only +/// noise for display: the end marker and a marker cut off at the end of a +/// streaming chunk. +public func separateContentAndContext(_ raw: String) -> ParsedMessageContent { + let (text, end) = extractEndMarker(Array(raw.utf16)) + let sections = splitSections(text) + return ParsedMessageContent( + content: string(stripStreamingTail(sections.content)), + contextString: sections.context.map(string), + contentHeader: sections.header.map(string), end: end) +} + +/// Whether the content holds openui-lang: the ```` ```openui-lang ```` fence, +/// or an unfenced top-level `root =` statement. +public func hasLangSyntax(_ content: String?) -> Bool { + guard let content, !content.isEmpty else { return false } + if content.contains("```openui-lang") { return true } + // /(^|\n)\s*root\s*=/ + let units = Array(content.utf16) + let root = Array("root".utf16) + for start in units.indices where start == 0 || units[start - 1] == newline { + var index = skipWhitespace(units, from: start) + guard units[index...].starts(with: root) else { continue } + index = skipWhitespace(units, from: index + root.count) + if index < units.count, units[index] == UInt16(ascii: "=") { return true } + } + return false +} + +// MARK: Messages react-ui's chat builds + +/// The user message react-ui's chat sends for a `continue_conversation` +/// action: the action's text as content, and as context what the user clicked +/// plus the form's values. +public func continueConversationMessage(_ event: ActionEvent) -> String { + let content = event.humanFriendlyMessage.isEmpty ? "" : wrapContent(event.humanFriendlyMessage) + var context: [OpenUIValue] = [.string("User clicked: \(event.humanFriendlyMessage)")] + if let formState = event.formState { context.append(.object(formState)) } + return content + wrapContext(JSON.stringify(.array(context))) +} + +/// An assistant message with the state its UI holds (form values, `$state`), +/// for saving with the conversation. Pass the parsed content and header; with +/// no state, the message is just the content. +public func messageWithState(content: String, contentHeader: String?, state: OpenUIObject) + -> String +{ + let message = wrapContentWithHeader(content, contentHeader) + guard !state.isEmpty else { return message } + return message + wrapContext(JSON.stringify(.array([.object(state)]))) +} + +/// The state saved in a message's context (see ``messageWithState``), for the +/// renderer's `initialState`. +public func initialState(fromContext contextString: String?) -> OpenUIObject? { + guard let contextString, let parsed = try? JSON.parse(contextString) else { return nil } + switch parsed { + case .array(let items): return items.first?.objectValue + case .object(let object): return object + default: return nil + } +} + +// MARK: Artifacts + +/// The kinds of artifact Cloud produces. +public enum ArtifactKind: String, Sendable, Equatable { + case slides + case report +} + +/// An artifact's header: which artifact, and what kind. +public struct ArtifactSentinelHeader: Sendable, Equatable { + public var artifactId: String + public var type: ArtifactKind + public var name: String? + public var version: String? +} + +/// Parses the artifact carrier `]]>openui:artifact \n`, +/// which arrives as a tool result. The program is empty on a reload that +/// stripped it. Returns `nil` for anything that isn't a valid carrier. +public func parseArtifactSentinel(_ raw: String) -> ( + header: ArtifactSentinelHeader, program: String +)? { + let units = Array(raw.utf16) + guard units.starts(with: artifactPrefix) else { return nil } + let newlineIndex = units.firstIndex(of: newline) + let header = string(units[artifactPrefix.count..<(newlineIndex ?? units.count)]) + let program = newlineIndex.map { string(units[($0 + 1)...]) } ?? "" + guard let fields = (try? JSON.parse(header))?.objectValue, + let id = fields["artifact_id"]?.stringValue, !id.isEmpty, + let type = fields["type"]?.stringValue.flatMap(artifactKind) + else { return nil } + let name = fields["name"]?.stringValue.flatMap { $0.isEmpty ? nil : $0 } + let version = fields["version"]?.stringValue.flatMap { $0.isEmpty ? nil : $0 } + return (ArtifactSentinelHeader(artifactId: id, type: type, name: name, version: version), program) +} + +private func artifactKind(_ type: String) -> ArtifactKind? { + switch type { + case "slides", "presentation": return .slides + case "report": return .report + default: return nil + } +} + +// MARK: - Sections + +private typealias Units = ArraySlice + +private func string(_ units: Units) -> String { String(decoding: units, as: UTF16.self) } + +/// The ordered walk over the inline-sentinel sections. +private func splitSections(_ raw: Units) -> (content: Units, context: Units?, header: Units?) { + let lastContent = lastIndex(of: contentMarker, in: raw) + let lastContext = lastIndex(of: contextMarker, in: raw) + + // No inline markers: fall back to the old XML envelope, so messages saved + // by older app versions still load. + guard lastContent != nil || lastContext != nil else { + let legacy = parseLegacyXML(raw) + return (legacy.content, legacy.context, nil) + } + guard let content = lastContent else { + return (stripSectionSeparator(raw[.. content else { + return (raw[bodyStart(raw, content)...], nil, header) + } + let body = raw[min(bodyStart(raw, content), context)..openui:end` marker line and reports whether there was +/// one. The newline before the marker goes with it, attributes on the marker +/// line are ignored, and text after the line stays with its section. +private func extractEndMarker(_ raw: [UInt16]) -> (Units, Bool) { + var text = Units(raw) + var end = false + while let index = firstIndex(of: endMarker, in: text) { + end = true + let before = stripSectionSeparator(text[.. Units { + var trim = 0 + for token in streamPartialTokens { + var length = min(token.count - 1, content.count) + while length > trim { + if content.suffix(length).elementsEqual(token.prefix(length)) { + trim = length + break + } + length -= 1 + } + } + return trim > 0 ? stripSectionSeparator(content.dropLast(trim)) : content +} + +private func bodyStart(_ raw: Units, _ marker: Int) -> Int { + firstIndex(of: newline, in: raw, from: marker).map { $0 + 1 } ?? raw.endIndex +} + +private func stripSectionSeparator(_ value: Units) -> Units { + if value.count >= 2, value.suffix(2).elementsEqual([carriageReturn, newline]) { + return value.dropLast(2) + } + return value.last == newline ? value.dropLast() : value +} + +/// The deprecated ``/`` XML envelope, kept only to read +/// messages saved before the inline markers. +private func parseLegacyXML(_ raw: Units) -> (content: Units, context: Units?) { + var content = raw + var context: Units? + + // /([\s\S]*)<\/context>\s*$/ + let open = Array("".utf16) + let close = Array("".utf16) + if let closeIndex = lastIndex(of: close, in: raw), + isWhitespace(raw[(closeIndex + close.count)...]), + let openIndex = firstIndex(of: open, in: raw), openIndex + open.count <= closeIndex + { + context = raw[(openIndex + open.count)..]*>([\s\S]*)<\/content>\s*$/ + let contentOpen = Array("".utf16) + if content.starts(with: contentOpen), + let tagEnd = firstIndex(of: [UInt16(ascii: ">")], in: content, from: content.startIndex), + let closeIndex = lastIndex(of: contentClose, in: content), closeIndex >= tagEnd + 1, + isWhitespace(content[(closeIndex + contentClose.count)...]) + { + content = content[(tagEnd + 1).. Int? { + var index = start ?? haystack.startIndex + while index + needle.count <= haystack.endIndex { + if haystack[index..<(index + needle.count)].elementsEqual(needle) { return index } + index += 1 + } + return nil +} + +private func firstIndex(of unit: UInt16, in haystack: Units, from start: Int) -> Int? { + haystack[start...].firstIndex(of: unit) +} + +private func lastIndex(of needle: [UInt16], in haystack: Units) -> Int? { + var index = haystack.endIndex - needle.count + while index >= haystack.startIndex { + if haystack[index..<(index + needle.count)].elementsEqual(needle) { return index } + index -= 1 + } + return nil +} + +private func isWhitespace(_ units: Units) -> Bool { + units.allSatisfy { Unicode.Scalar($0).map(jsWhitespace.contains) ?? false } +} + +private func skipWhitespace(_ units: [UInt16], from start: Int) -> Int { + var index = start + while index < units.count, Unicode.Scalar(units[index]).map(jsWhitespace.contains) ?? false { + index += 1 + } + return index +} diff --git a/packages/swift-lang/Sources/OpenUILang/EvaluateTree.swift b/packages/swift-lang/Sources/OpenUILang/EvaluateTree.swift new file mode 100644 index 0000000000..112e58d541 --- /dev/null +++ b/packages/swift-lang/Sources/OpenUILang/EvaluateTree.swift @@ -0,0 +1,82 @@ +/// Evaluates one prop value with schema awareness: AST nodes, reactive +/// bindings, nested elements, arrays and action plans. The inline and tree +/// paths differ only in how they recurse, so they pass that in. +func evaluatePropCore( + _ value: OpenUIValue, _ context: EvaluationContext, _ schema: SchemaContext, reactive: Bool, + recurseElement: (ElementNode) -> ElementNode, + recurse: (OpenUIValue, Bool) -> OpenUIValue +) -> OpenUIValue { + switch value { + case .undefined, .null, .bool, .number, .string: + return value + + case .ast(let node): + // A $state on a reactive prop becomes a two-way binding. + if case .stateRef(let name) = node, reactive { + return .reactiveAssign(ReactiveAssign(target: name, expr: .stateRef("$value"))) + } + let result = evaluate(node, context, schema) + switch result { + case .element(let element): + return .element(recurseElement(element)) + case .array(let items): + return .array( + items.map { item in + if case .element(let element) = item { return .element(recurseElement(element)) } + return item + }) + case .reactiveAssign(let assign) where !reactive: + let current = context.getState(assign.target) + return current.isNullish ? .null : current + default: + return result + } + + case .array(let items): + return .array(items.map { recurse($0, reactive) }) + + case .element(let element): + return .element(recurseElement(element)) + + case .actionPlan, .actionStep, .reactiveAssign: + // Evaluated when the action fires or the field is set, not now. + return value + + case .object(let object): + let needsEval = object.values.contains { member in + switch member { + case .undefined, .null, .bool, .number, .string: return false + default: return true + } + } + guard needsEval else { return value } + var result = OpenUIObject() + for (key, member) in object { result[key] = recurse(member, reactive) } + return .object(result) + } +} + +/// Evaluates every AST node in an element's props (recursively), returning an +/// element whose props are concrete values ready to render. +public func evaluateElementProps( + _ element: ElementNode, _ context: EvaluationContext, _ schema: SchemaContext +) -> ElementNode { + if element.hasDynamicProps == false { return element } + var evaluated = OpenUIObject() + for (key, value) in element.props { + evaluated[key] = evaluatePropValue( + value, context, schema, reactive: schema.isReactiveProp(element.typeName, key)) + } + var result = element + result.props = evaluated + return result +} + +private func evaluatePropValue( + _ value: OpenUIValue, _ context: EvaluationContext, _ schema: SchemaContext, reactive: Bool +) -> OpenUIValue { + evaluatePropCore( + value, context, schema, reactive: reactive, + recurseElement: { evaluateElementProps($0, context, schema) }, + recurse: { evaluatePropValue($0, context, schema, reactive: $1) }) +} diff --git a/packages/swift-lang/Sources/OpenUILang/Evaluator.swift b/packages/swift-lang/Sources/OpenUILang/Evaluator.swift new file mode 100644 index 0000000000..4e998a835f --- /dev/null +++ b/packages/swift-lang/Sources/OpenUILang/Evaluator.swift @@ -0,0 +1,346 @@ +/// Supplies `$state` values and statement references to the evaluator. +public struct EvaluationContext { + /// Reads a `$variable` from the store. + public var getState: (String) -> OpenUIValue + /// Resolves a reference to another declaration's value (Query/Mutation results). + public var resolveRef: (String) -> OpenUIValue + /// Extra scope for `$value` while applying a reactive assignment. + public var extraScope: OpenUIObject? + + public init( + getState: @escaping (String) -> OpenUIValue, + resolveRef: @escaping (String) -> OpenUIValue = { _ in .undefined }, + extraScope: OpenUIObject? = nil + ) { + self.getState = getState + self.resolveRef = resolveRef + self.extraScope = extraScope + } +} + +/// Library knowledge the evaluator needs: which component props bind to +/// `$state` two-way (lang-core's `reactive()` schemas). +public struct SchemaContext { + public var isReactiveProp: (_ component: String, _ prop: String) -> Bool + + public init(isReactiveProp: @escaping (String, String) -> Bool) { + self.isReactiveProp = isReactiveProp + } +} + +/// Evaluates an AST node to a runtime value. +public func evaluate( + _ node: ASTNode, _ context: EvaluationContext, _ schema: SchemaContext? = nil +) -> OpenUIValue { + switch node { + case .str(let value): return .string(value) + case .num(let value): return .number(value) + case .bool(let value): return .bool(value) + case .null, .ph: return .null + + case .stateRef(let name): + if let scoped = context.extraScope?[name], !scoped.isNullish { return scoped } + return context.getState(name) + + case .ref(let name), .runtimeRef(let name, _): + return context.resolveRef(name) + + case .arr(let elements): + return .array(elements.map { evaluate($0, context) }) + + case .obj(let entries): + var object = OpenUIObject() + for entry in entries { object[entry.key] = evaluate(entry.value, context) } + return .object(object) + + case .comp(let name, let args, let mappedProps): + if lazyBuiltins.contains(name) { return evaluateLazyBuiltin(name, args, context, schema) } + if let builtin = builtins[name] { return builtin.fn(args.map { evaluate($0, context) }) } + if actionNames.contains(name) { return evaluateActionCall(name, args, context) } + if let mappedProps { return evaluateMappedComponent(name, mappedProps, context, schema) } + // Every catalog component is lowered to an element at parse time, so an + // unmapped call here is unexpected. + return .null + + case .binOp(let op, let left, let right): + return evaluateBinary(op, left, right, context) + + case .unaryOp(let op, let operand): + switch op { + case "!": return .bool(!evaluate(operand, context).isTruthy) + case "-": return .number(-toNumber(evaluate(operand, context))) + default: return .null + } + + case .ternary(let cond, let then, let otherwise): + return evaluate(cond, context).isTruthy ? evaluate(then, context) : evaluate(otherwise, context) + + case .member(let objNode, let field): + let obj = evaluate(objNode, context) + if obj.isNullish { return .null } + // Array pluck: `rows.name` maps over the items. + if case .array(let items) = obj { + if field == "length" { return .number(Double(items.count)) } + return .array( + items.map { item in + if item.isNullish { return .null } + let value = jsProperty(item, field) + return value.isNullish ? .null : value + }) + } + return jsProperty(obj, field) + + case .index(let objNode, let indexNode): + let obj = evaluate(objNode, context) + let index = evaluate(indexNode, context) + if obj.isNullish || index.isNullish { return .null } + if case .array = obj { return jsProperty(obj, jsNumberToString(toNumber(index))) } + return jsProperty(obj, index.jsString) + + case .assign(let target, let value): + return .reactiveAssign(ReactiveAssign(target: target, expr: value)) + } +} + +private func evaluateBinary( + _ op: String, _ leftNode: ASTNode, _ rightNode: ASTNode, _ context: EvaluationContext +) -> OpenUIValue { + // Short-circuit operators return an operand, like JavaScript. + if op == "&&" { + let left = evaluate(leftNode, context) + return left.isTruthy ? evaluate(rightNode, context) : left + } + if op == "||" { + let left = evaluate(leftNode, context) + return left.isTruthy ? left : evaluate(rightNode, context) + } + let left = evaluate(leftNode, context) + let right = evaluate(rightNode, context) + switch op { + case "+": + if left.jsTypeof == "string" || right.jsTypeof == "string" { + // null/undefined concatenate as "" rather than "null". + let l = left.isNullish ? "" : left.jsString + let r = right.isNullish ? "" : right.jsString + return .string(l + r) + } + return .number(toNumber(left) + toNumber(right)) + case "-": return .number(toNumber(left) - toNumber(right)) + case "*": return .number(toNumber(left) * toNumber(right)) + case "/": + // Division by zero is 0, a deliberate DSL choice over Infinity/NaN. + return .number(toNumber(right) == 0 ? 0 : toNumber(left) / toNumber(right)) + case "%": + return .number( + toNumber(right) == 0 ? 0 : toNumber(left).truncatingRemainder(dividingBy: toNumber(right))) + case "==": return .bool(left.looselyEquals(right)) + case "!=": return .bool(!left.looselyEquals(right)) + case ">": return .bool(toNumber(left) > toNumber(right)) + case "<": return .bool(toNumber(left) < toNumber(right)) + case ">=": return .bool(toNumber(left) >= toNumber(right)) + case "<=": return .bool(toNumber(left) <= toNumber(right)) + default: return .null + } +} + +/// A catalog component inside a runtime expression (a ternary branch, an +/// `@Each` template): builds its element from the mapped props. +private func evaluateMappedComponent( + _ name: String, _ mappedProps: [ASTEntry], _ context: EvaluationContext, + _ schema: SchemaContext? +) -> OpenUIValue { + var props = OpenUIObject() + for entry in mappedProps { + if case .stateRef(let stateName) = entry.value { + if let schema, schema.isReactiveProp(name, entry.key) { + // Reactive prop bound to $state: emit a two-way binding. + props[entry.key] = .reactiveAssign( + ReactiveAssign(target: stateName, expr: .stateRef("$value"))) + } else { + // Without schema context the StateRef stays AST for evaluateElementProps. + props[entry.key] = schema != nil ? context.getState(stateName) : .ast(entry.value) + } + } else { + props[entry.key] = evaluate(entry.value, context, schema) + } + } + if let schema { + for (key, value) in props { + switch value { + case .element(let element): + props[key] = .element(evaluateElementInline(element, context, schema)) + case .array(let items): + props[key] = .array( + items.map { item in + if case .element(let element) = item { + return .element(evaluateElementInline(element, context, schema)) + } + return item + }) + default: + break + } + } + } + return .element(ElementNode(typeName: name, props: props, partial: false, hasDynamicProps: true)) +} + +/// Resolves a reactive assignment to the current state value outside a reactive prop. +public func stripReactiveAssign(_ value: OpenUIValue, _ context: EvaluationContext) -> OpenUIValue { + guard case .reactiveAssign(let assign) = value else { return value } + let current = context.getState(assign.target) + return current.isNullish ? .null : current +} + +/// Evaluates an element's props with schema awareness (inline path). +func evaluateElementInline( + _ element: ElementNode, _ context: EvaluationContext, _ schema: SchemaContext +) -> ElementNode { + if element.hasDynamicProps == false { return element } + var evaluated = OpenUIObject() + for (key, value) in element.props { + evaluated[key] = evaluatePropInline( + value, context, schema, reactive: schema.isReactiveProp(element.typeName, key)) + } + var result = element + result.props = evaluated + return result +} + +private func evaluatePropInline( + _ value: OpenUIValue, _ context: EvaluationContext, _ schema: SchemaContext, reactive: Bool +) -> OpenUIValue { + evaluatePropCore( + value, context, schema, reactive: reactive, + recurseElement: { evaluateElementInline($0, context, schema) }, + recurse: { evaluatePropInline($0, context, schema, reactive: $1) }) +} + +/// Converts a runtime value back to a literal AST node so it can be captured in +/// a deferred expression (an action evaluated at click time). +func toLiteralAST(_ value: OpenUIValue) -> ASTNode { + switch value { + case .undefined, .null: return .null + case .string(let string): return .str(string) + case .number(let number): return .num(number) + case .bool(let bool): return .bool(bool) + case .array(let items): return .arr(items.map(toLiteralAST)) + case .object(let object): + return .obj(object.entries.map { ASTEntry($0.key, toLiteralAST($0.value)) }) + default: + // Elements, AST and action markers are plain objects in JavaScript. + return toLiteralAST(value.jsonRepresentation) + } +} + +/// `Action`, `Run`, `ToAssistant`, `OpenUrl`, `Set` and `Reset` calls. +private func evaluateActionCall(_ name: String, _ args: [ASTNode], _ context: EvaluationContext) + -> OpenUIValue +{ + func stringArg(_ index: Int) -> String { + let value = evaluate(args[index], context) + return value.isNullish ? "" : value.jsString + } + switch name { + case "Action": + let stepsValue = args.isEmpty ? .array([]) : evaluate(args[0], context) + let steps: [ActionStep] = (stepsValue.arrayValue ?? []).compactMap { + if case .actionStep(let step) = $0 { return step } + return nil + } + return .actionPlan(ActionPlan(steps: steps)) + case "Run": + // An unresolved ref yields null, which Action drops. + guard case .runtimeRef(let statementId, let refType)? = args.first else { return .null } + return .actionStep(.run(statementId: statementId, refType: refType)) + case "ToAssistant": + return .actionStep( + .continueConversation( + message: args.isEmpty ? "" : stringArg(0), context: args.count > 1 ? stringArg(1) : nil)) + case "OpenUrl": + return .actionStep(.openUrl(url: args.isEmpty ? "" : stringArg(0))) + case "Set": + // The value expression is kept as AST and evaluated when the action fires. + guard args.count >= 2, case .stateRef(let target) = args[0] else { return .null } + return .actionStep(.set(target: target, valueAST: args[1])) + case "Reset": + let targets = args.compactMap { arg -> String? in + if case .stateRef(let name) = arg { return name } + return nil + } + return targets.isEmpty ? .null : .actionStep(.reset(targets: targets)) + default: + return .null + } +} + +/// Replaces `Ref(varName)` with the loop item's literal value, so deferred +/// expressions (action steps) keep the item after the loop has finished. +func substituteRef(_ node: ASTNode, _ varName: String, _ value: OpenUIValue) -> ASTNode { + switch node { + case .ref(let name): + return name == varName ? toLiteralAST(value) : node + case .member(let obj, let field): + let substituted = substituteRef(obj, varName, value) + // `t.id` on a now-literal object resolves immediately. + if case .obj(let entries) = substituted, let entry = entries.first(where: { $0.key == field }) { + return entry.value + } + return .member(obj: substituted, field: field) + case .index(let obj, let index): + return .index( + obj: substituteRef(obj, varName, value), index: substituteRef(index, varName, value)) + case .binOp(let op, let left, let right): + return .binOp( + op: op, left: substituteRef(left, varName, value), right: substituteRef(right, varName, value) + ) + case .unaryOp(let op, let operand): + return .unaryOp(op: op, operand: substituteRef(operand, varName, value)) + case .ternary(let cond, let then, let otherwise): + return .ternary( + cond: substituteRef(cond, varName, value), then: substituteRef(then, varName, value), + else: substituteRef(otherwise, varName, value)) + case .arr(let elements): + return .arr(elements.map { substituteRef($0, varName, value) }) + case .obj(let entries): + return .obj(entries.map { ASTEntry($0.key, substituteRef($0.value, varName, value)) }) + case .comp(let name, let args, let mappedProps): + return .comp( + name: name, args: args.map { substituteRef($0, varName, value) }, + mappedProps: mappedProps?.map { ASTEntry($0.key, substituteRef($0.value, varName, value)) }) + case .assign(let target, let assigned): + return .assign(target: target, value: substituteRef(assigned, varName, value)) + default: + return node + } +} + +/// `@Each(array, varName, template)`: evaluates the template once per item. +private func evaluateLazyBuiltin( + _ name: String, _ args: [ASTNode], _ context: EvaluationContext, _ schema: SchemaContext? +) -> OpenUIValue { + guard name == "Each" else { return .null } + guard args.count >= 3, case .array(let items) = evaluate(args[0], context) else { + return .array([]) + } + let varName: String + switch args[1] { + case .ref(let n): varName = n + case .str(let v): varName = v + default: return .array([]) + } + let template = args[2] + return .array( + items.map { item in + let substituted = substituteRef(template, varName, item) + var childContext = context + childContext.resolveRef = { refName in + refName == varName ? item : context.resolveRef(refName) + } + let result = evaluate(substituted, childContext, schema) + if let schema, case .element(let element) = result { + return .element(evaluateElementInline(element, childContext, schema)) + } + return result + }) +} diff --git a/packages/swift-lang/Sources/OpenUILang/Expressions.swift b/packages/swift-lang/Sources/OpenUILang/Expressions.swift new file mode 100644 index 0000000000..80e3f15e13 --- /dev/null +++ b/packages/swift-lang/Sources/OpenUILang/Expressions.swift @@ -0,0 +1,224 @@ +// Precedence levels (spec section 2.11). +private let precTernary = 1 +private let precOr = 2 +private let precAnd = 3 +private let precEq = 4 +private let precCmp = 5 +private let precAdd = 6 +private let precMul = 7 +private let precUnary = 8 +private let precMember = 9 + +/// `name.replace(/^\$/, "")` +private func dropDollar(_ name: String) -> String { + name.hasPrefix("$") ? String(name.dropFirst()) : name +} + +/// Parses a statement's tokens into an AST with a Pratt (top-down operator +/// precedence) parser. Malformed input never throws: unknown tokens become +/// `Null` and missing closers are tolerated. +public func parseExpression(_ tokens: [Token]) -> ASTNode { + var parser = ExpressionParser(tokens: tokens) + return parser.parseExpr(0) +} + +private struct ExpressionParser { + let tokens: [Token] + var pos = 0 + + var cur: Token { pos < tokens.count ? tokens[pos] : Token(.eof) } + + @discardableResult + mutating func adv() -> Token { + let token = cur + pos += 1 + return token + } + + mutating func eat(_ type: TokenType) { + if cur.type == type { pos += 1 } + } + + func peekType(_ offset: Int) -> TokenType? { + pos + offset < tokens.count ? tokens[pos + offset].type : nil + } + + func infixPrecedence(_ token: Token) -> Int { + switch token.type { + case .question: return precTernary + case .or: return precOr + case .and: return precAnd + case .eqEq, .notEq: return precEq + case .greater, .less, .greaterEq, .lessEq: return precCmp + case .plus, .minus: return precAdd + case .star, .slash, .percent: return precMul + case .dot, .lBrack: return precMember + default: return 0 + } + } + + mutating func parseExpr(_ minPrec: Int) -> ASTNode { + var left = parsePrefix() + while infixPrecedence(cur) > minPrec { + left = parseInfix(left) + } + return left + } + + mutating func parsePrefix() -> ASTNode { + let token = cur + switch token.type { + case .str: + adv() + return .str(token.string ?? "") + case .num: + adv() + return .num(token.number ?? .nan) + case .true: + adv() + return .bool(true) + case .false: + adv() + return .bool(false) + case .null: + adv() + return .null + case .lBrack: + return parseArr() + case .lBrace: + return parseObj() + case .stateVar: + // `$var = expr` is an assignment (single `=`, not `==`). + let name = token.string ?? "" + adv() + if cur.type == .equals { + adv() + return .assign(target: name, value: parseExpr(0)) + } + return .stateRef(name) + case .type: + // Builtins need the @ prefix to be called; only Action is exempt. + let name = token.string ?? "" + if peekType(1) == .lParen && (!isBuiltin(name) || name == "Action") { return parseComp() } + adv() + return .ref(name) + case .builtinCall: + if peekType(1) == .lParen { return parseComp() } + adv() + return .ref(token.string ?? "") + case .ident: + adv() + return .ref(token.string ?? "") + case .not: + adv() + return .unaryOp(op: "!", operand: parseExpr(precUnary)) + case .minus: + adv() + return .unaryOp(op: "-", operand: parseExpr(precUnary)) + case .lParen: + adv() + let inner = parseExpr(0) + eat(.rParen) + return inner + default: + // Unknown token: skip it. + adv() + return .null + } + } + + mutating func parseInfix(_ left: ASTNode) -> ASTNode { + let token = cur + func binary(_ op: String, _ prec: Int) -> ASTNode { + adv() + return .binOp(op: op, left: left, right: parseExpr(prec)) + } + switch token.type { + case .plus: return binary("+", precAdd) + case .minus: return binary("-", precAdd) + case .star: return binary("*", precMul) + case .slash: return binary("/", precMul) + case .percent: return binary("%", precMul) + case .eqEq: return binary("==", precEq) + case .notEq: return binary("!=", precEq) + case .greater: return binary(">", precCmp) + case .less: return binary("<", precCmp) + case .greaterEq: return binary(">=", precCmp) + case .lessEq: return binary("<=", precCmp) + case .and: return binary("&&", precAnd) + case .or: return binary("||", precOr) + case .question: + // Right-associative: both branches parse at the lowest precedence. + adv() + let then = parseExpr(0) + eat(.colon) + let otherwise = parseExpr(0) + return .ternary(cond: left, then: then, else: otherwise) + case .dot: + adv() + let fieldToken = cur + adv() + let field: String + switch fieldToken.type { + case .ident, .type, .str, .num: field = fieldToken.valueString + case .stateVar: field = dropDollar(fieldToken.string ?? "") + default: field = "?" + } + return .member(obj: left, field: field) + case .lBrack: + adv() + let index = parseExpr(0) + eat(.rBrack) + return .index(obj: left, index: index) + default: + return left + } + } + + /// `TypeName(arg1, arg2, ...)` + mutating func parseComp() -> ASTNode { + let name = cur.string ?? "" + adv() + eat(.lParen) + var args: [ASTNode] = [] + while cur.type != .rParen && cur.type != .eof { + args.append(parseExpr(0)) + if cur.type == .comma { adv() } + } + eat(.rParen) + return .comp(name: name, args: args, mappedProps: nil) + } + + /// `[elem1, elem2, ...]` + mutating func parseArr() -> ASTNode { + adv() + var elements: [ASTNode] = [] + while cur.type != .rBrack && cur.type != .eof { + elements.append(parseExpr(0)) + if cur.type == .comma { adv() } + } + eat(.rBrack) + return .arr(elements) + } + + /// `{ key: value, ... }` + mutating func parseObj() -> ASTNode { + adv() + var entries: [ASTEntry] = [] + while cur.type != .rBrace && cur.type != .eof { + let keyToken = cur + adv() + let key: String + switch keyToken.type { + case .ident, .str, .type, .num: key = keyToken.valueString + case .stateVar: key = dropDollar(keyToken.string ?? "") + default: key = "?" + } + eat(.colon) + entries.append(ASTEntry(key, parseExpr(0))) + if cur.type == .comma { adv() } + } + eat(.rBrace) + return .obj(entries) + } +} diff --git a/packages/swift-lang/Sources/OpenUILang/FormRules.swift b/packages/swift-lang/Sources/OpenUILang/FormRules.swift new file mode 100644 index 0000000000..d31610b4de --- /dev/null +++ b/packages/swift-lang/Sources/OpenUILang/FormRules.swift @@ -0,0 +1,203 @@ +import Foundation + +/// A validation rule such as `required`, `min:8` or `pattern:^[a-z]`. +public struct ParsedRule: Sendable, Equatable { + public var type: String + public var arg: OpenUIValue? + + public init(type: String, arg: OpenUIValue? = nil) { + self.type = type + self.arg = arg + } +} + +private let numericRules: Set = ["min", "max", "minLength", "maxLength"] + +/// `"min:8"` → `{ type: "min", arg: 8 }`. +public func parseRule(_ rule: String) -> ParsedRule { + guard let colon = rule.firstIndex(of: ":") else { return ParsedRule(type: rule) } + let type = String(rule[.. [ParsedRule] { + (rules.arrayValue ?? []).compactMap(\.stringValue).map(parseRule) +} + +/// Parses a structured rules object: `{ required: true, minLength: 5 }`. +/// `false` and `null` entries are skipped. +public func parseStructuredRules(_ rules: OpenUIValue) -> [ParsedRule] { + guard case .object(let object) = rules else { return [] } + return object.entries.compactMap { key, value in + switch value { + case .bool(false), .null, .undefined: return nil + case .bool(true): return ParsedRule(type: key) + default: return ParsedRule(type: key, arg: value) + } + } +} + +public typealias ValidatorFn = @Sendable (OpenUIValue, OpenUIValue?) -> String? + +private func isEmpty(_ value: OpenUIValue) -> Bool { + switch value { + case .undefined, .null, .string(""): return true + case .array(let items): return items.isEmpty + case .object(let object): + // Form state stores { value, componentType }; check the actual value. + if let inner = object["value"] { return isEmpty(inner) } + // An empty object, e.g. a checkbox group with nothing selected. + return object.isEmpty + default: return false + } +} + +/// JavaScript `parseFloat`: the longest prefix that is a decimal literal, or NaN. +func jsParseFloat(_ string: String) -> Double { + let units = Array(jsTrim(string).utf8) + var i = 0 + func isDigit(_ index: Int) -> Bool { + index < units.count && units[index] >= UInt8(ascii: "0") && units[index] <= UInt8(ascii: "9") + } + var sign = "" + if i < units.count, units[i] == UInt8(ascii: "+") || units[i] == UInt8(ascii: "-") { + sign = units[i] == UInt8(ascii: "-") ? "-" : "" + i += 1 + } + if Array(units[i...]).starts(with: Array("Infinity".utf8)) { + return sign == "-" ? -.infinity : .infinity + } + var integer = "" + while isDigit(i) { + integer.append(Character(UnicodeScalar(units[i]))) + i += 1 + } + var fraction = "" + if i < units.count, units[i] == UInt8(ascii: ".") { + var j = i + 1 + while isDigit(j) { + fraction.append(Character(UnicodeScalar(units[j]))) + j += 1 + } + if !integer.isEmpty || !fraction.isEmpty { i = j } + } + guard !integer.isEmpty || !fraction.isEmpty else { return .nan } + var exponent = "" + if i < units.count, units[i] == UInt8(ascii: "e") || units[i] == UInt8(ascii: "E") { + var j = i + 1 + var expSign = "" + if j < units.count, units[j] == UInt8(ascii: "+") || units[j] == UInt8(ascii: "-") { + expSign = units[j] == UInt8(ascii: "-") ? "-" : "" + j += 1 + } + var digits = "" + while isDigit(j) { + digits.append(Character(UnicodeScalar(units[j]))) + j += 1 + } + if !digits.isEmpty { exponent = "e" + expSign + digits } + } + let literal = + sign + (integer.isEmpty ? "0" : integer) + (fraction.isEmpty ? "" : "." + fraction) + exponent + return Double(literal) ?? .nan +} + +private func numberArg(_ arg: OpenUIValue?) -> Double { + switch arg { + case .number(let number)?: return number + case .string(let string)?: return jsStringToNumber(string) + default: return .nan + } +} + +/// The built-in validators, keyed by rule type. Messages match lang-core. +public let builtInValidators: [String: ValidatorFn] = [ + "required": { value, _ in + if isEmpty(value) { return "This field is required" } + if case .object(let object) = value { + let values = object.values + let allBooleans = values.allSatisfy { $0.boolValue != nil } + if !values.isEmpty, allBooleans, !values.contains(.bool(true)) { + return "At least one option is required" + } + } + return nil + }, + "email": { value, _ in + if isEmpty(value) { return nil } + guard case .string(let string) = value else { return "Please enter a valid email" } + return string.range(of: #"^[^\s@]+@[^\s@]+\.[^\s@]+$"#, options: .regularExpression) != nil + ? nil : "Please enter a valid email" + }, + "url": { value, _ in + if isEmpty(value) { return nil } + guard case .string(let string) = value else { return "Please enter a valid URL" } + // `new URL(value)` requires an absolute URL with a scheme. + let hasScheme = + string.range(of: #"^[A-Za-z][A-Za-z0-9+.\-]*:"#, options: .regularExpression) != nil + return hasScheme && URL(string: string) != nil ? nil : "Please enter a valid URL" + }, + "numeric": { value, _ in + if isEmpty(value) { return nil } + if case .number(let number) = value, !number.isNaN { return nil } + if case .string(let string) = value, !jsParseFloat(string).isNaN, !jsTrim(string).isEmpty { + return nil + } + return "Must be a number" + }, + "min": { value, arg in + if isEmpty(value) { return nil } + let number = value.numberValue ?? jsParseFloat(value.jsString) + if number.isNaN { return nil } + let min = numberArg(arg) + return number >= min ? nil : "Must be at least \(jsNumberToString(min))" + }, + "max": { value, arg in + if isEmpty(value) { return nil } + let number = value.numberValue ?? jsParseFloat(value.jsString) + if number.isNaN { return nil } + let max = numberArg(arg) + return number <= max ? nil : "Must be no more than \(jsNumberToString(max))" + }, + "minLength": { value, arg in + if isEmpty(value) { return nil } + guard case .string(let string) = value else { return nil } + let min = numberArg(arg) + return Double(string.utf16.count) >= min + ? nil : "Must be at least \(jsNumberToString(min)) characters" + }, + "maxLength": { value, arg in + if isEmpty(value) { return nil } + guard case .string(let string) = value else { return nil } + let max = numberArg(arg) + return Double(string.utf16.count) <= max + ? nil : "Must be no more than \(jsNumberToString(max)) characters" + }, + "pattern": { value, arg in + if isEmpty(value) { return nil } + guard case .string(let string) = value, case .string(let pattern)? = arg, + let regex = try? NSRegularExpression(pattern: pattern) + else { return nil } + let range = NSRange(string.startIndex..., in: string) + return regex.firstMatch(in: string, range: range) != nil ? nil : "Invalid format" + }, +] + +/// Runs rules in order and returns the first error. Custom validators take +/// precedence over built-in ones; unknown rule types are skipped. +public func validate( + _ value: OpenUIValue, _ rules: [ParsedRule], customValidators: [String: ValidatorFn] = [:] +) -> String? { + for rule in rules { + guard let validator = customValidators[rule.type] ?? builtInValidators[rule.type] else { + continue + } + if let error = validator(value, rule.arg) { return error } + } + return nil +} diff --git a/packages/swift-lang/Sources/OpenUILang/JSON.swift b/packages/swift-lang/Sources/OpenUILang/JSON.swift new file mode 100644 index 0000000000..c8f58b4cf6 --- /dev/null +++ b/packages/swift-lang/Sources/OpenUILang/JSON.swift @@ -0,0 +1,439 @@ +/// JSON reading and writing with JavaScript semantics. +/// +/// Reading preserves object key order, which matters because a library's JSON +/// Schema lists props in positional-argument order. Writing matches +/// `JSON.stringify`, so Swift output can be compared with lang-core's. +public enum JSON { + public struct SyntaxError: Error, CustomStringConvertible { + public var description: String + } + + /// `JSON.parse`. Objects keep JavaScript key order. + public static func parse(_ text: String) throws -> OpenUIValue { + var reader = Reader(units: Array(text.utf16)) + reader.skipWhitespace() + let value = try reader.readValue() + reader.skipWhitespace() + guard reader.index == reader.units.count else { + throw SyntaxError(description: "Unexpected trailing characters at \(reader.index)") + } + return value + } + + /// `JSON.stringify` without indentation. `undefined` object members are + /// omitted, `undefined` array items and non-finite numbers become `null`. + public static func stringify(_ value: OpenUIValue) -> String { + var out = "" + write(value.jsonRepresentation, into: &out) + return out + } + + private static func write(_ value: OpenUIValue, into out: inout String) { + switch value { + case .undefined, .null: + out += "null" + case .bool(let bool): + out += bool ? "true" : "false" + case .number(let number): + out += number.isFinite ? jsNumberToString(number) : "null" + case .string(let string): + writeString(string, into: &out) + case .array(let items): + out += "[" + for (offset, item) in items.enumerated() { + if offset > 0 { out += "," } + write(item, into: &out) + } + out += "]" + case .object(let object): + out += "{" + var first = true + for (key, member) in object where member != .undefined { + if !first { out += "," } + first = false + writeString(key, into: &out) + out += ":" + write(member, into: &out) + } + out += "}" + default: + write(value.jsonRepresentation, into: &out) + } + } + + private static func writeString(_ string: String, into out: inout String) { + out += "\"" + for scalar in string.unicodeScalars { + switch scalar { + case "\"": out += "\\\"" + case "\\": out += "\\\\" + case "\u{08}": out += "\\b" + case "\u{0C}": out += "\\f" + case "\n": out += "\\n" + case "\r": out += "\\r" + case "\t": out += "\\t" + default: + if scalar.value < 0x20 { + let hex = String(scalar.value, radix: 16) + out += "\\u" + String(repeating: "0", count: 4 - hex.count) + hex + } else { + out.unicodeScalars.append(scalar) + } + } + } + out += "\"" + } + + // MARK: Reader + + struct Reader { + let units: [UInt16] + var index = 0 + + mutating func skipWhitespace() { + while index < units.count, + units[index] == 0x20 || units[index] == 0x09 || units[index] == 0x0A + || units[index] == 0x0D + { + index += 1 + } + } + + mutating func readValue() throws -> OpenUIValue { + guard index < units.count else { throw SyntaxError(description: "Unexpected end of JSON") } + switch units[index] { + case UInt16(ascii: "{"): return try readObject() + case UInt16(ascii: "["): return try readArray() + case UInt16(ascii: "\""): return .string(try readString()) + case UInt16(ascii: "t"): return try readKeyword("true", .bool(true)) + case UInt16(ascii: "f"): return try readKeyword("false", .bool(false)) + case UInt16(ascii: "n"): return try readKeyword("null", .null) + default: return try readNumber() + } + } + + mutating func readKeyword(_ word: String, _ value: OpenUIValue) throws -> OpenUIValue { + let wordUnits = Array(word.utf16) + guard index + wordUnits.count <= units.count, + Array(units[index.. OpenUIValue { + let start = index + if index < units.count, units[index] == UInt16(ascii: "-") { index += 1 } + while index < units.count, + (UInt16(ascii: "0")...UInt16(ascii: "9")).contains(units[index]) + || units[index] == UInt16(ascii: ".") || units[index] == UInt16(ascii: "e") + || units[index] == UInt16(ascii: "E") || units[index] == UInt16(ascii: "+") + || units[index] == UInt16(ascii: "-") + { + index += 1 + } + let text = String(decoding: units[start.. String { + index += 1 // opening quote + let start = index + while index < units.count { + if units[index] == UInt16(ascii: "\\") { + index += 2 + } else if units[index] == UInt16(ascii: "\"") { + let body = Array(units[start.. OpenUIValue { + index += 1 + var items: [OpenUIValue] = [] + skipWhitespace() + if index < units.count, units[index] == UInt16(ascii: "]") { + index += 1 + return .array(items) + } + while true { + skipWhitespace() + items.append(try readValue()) + skipWhitespace() + guard index < units.count else { throw SyntaxError(description: "Unterminated array") } + if units[index] == UInt16(ascii: ",") { + index += 1 + } else if units[index] == UInt16(ascii: "]") { + index += 1 + return .array(items) + } else { + throw SyntaxError(description: "Expected , or ] at \(index)") + } + } + } + + mutating func readObject() throws -> OpenUIValue { + index += 1 + var object = OpenUIObject() + skipWhitespace() + if index < units.count, units[index] == UInt16(ascii: "}") { + index += 1 + return .object(object) + } + while true { + skipWhitespace() + guard index < units.count, units[index] == UInt16(ascii: "\"") else { + throw SyntaxError(description: "Expected key at \(index)") + } + let key = try readString() + skipWhitespace() + guard index < units.count, units[index] == UInt16(ascii: ":") else { + throw SyntaxError(description: "Expected : at \(index)") + } + index += 1 + skipWhitespace() + object[key] = try readValue() + skipWhitespace() + guard index < units.count else { throw SyntaxError(description: "Unterminated object") } + if units[index] == UInt16(ascii: ",") { + index += 1 + } else if units[index] == UInt16(ascii: "}") { + index += 1 + return .object(object) + } else { + throw SyntaxError(description: "Expected , or } at \(index)") + } + } + } + } +} + +/// Decodes the body of a JSON string literal (without the quotes), following +/// `JSON.parse`: returns `nil` for raw control characters and invalid escapes. +/// Lone surrogates, which JavaScript strings can hold but Swift strings cannot, +/// decode to U+FFFD. +func decodeJSONStringBody(_ body: [UInt16]) -> String? { + var out: [UInt16] = [] + out.reserveCapacity(body.count) + var i = 0 + while i < body.count { + let unit = body[i] + if unit < 0x20 { return nil } + if unit != UInt16(ascii: "\\") { + out.append(unit) + i += 1 + continue + } + guard i + 1 < body.count else { return nil } + let escaped = body[i + 1] + i += 2 + switch escaped { + case UInt16(ascii: "\""): out.append(UInt16(ascii: "\"")) + case UInt16(ascii: "\\"): out.append(UInt16(ascii: "\\")) + case UInt16(ascii: "/"): out.append(UInt16(ascii: "/")) + case UInt16(ascii: "b"): out.append(0x08) + case UInt16(ascii: "f"): out.append(0x0C) + case UInt16(ascii: "n"): out.append(0x0A) + case UInt16(ascii: "r"): out.append(0x0D) + case UInt16(ascii: "t"): out.append(0x09) + case UInt16(ascii: "u"): + guard i + 4 <= body.count, + let code = UInt16(String(decoding: body[i.. Bool { + switch (self, other) { + case (.undefined, .undefined), (.null, .null), (.undefined, .null), (.null, .undefined): + return true + case (.undefined, _), (.null, _), (_, .undefined), (_, .null): + return false + case (.number(let lhs), .number(let rhs)): return lhs == rhs + case (.string(let lhs), .string(let rhs)): return lhs == rhs + case (.bool(let lhs), .bool(let rhs)): return lhs == rhs + case (.number(let lhs), .string(let rhs)): return lhs == jsStringToNumber(rhs) + case (.string(let lhs), .number(let rhs)): return jsStringToNumber(lhs) == rhs + case (.bool(let lhs), _): return OpenUIValue.number(lhs ? 1 : 0).looselyEquals(other) + case (_, .bool(let rhs)): return looselyEquals(.number(rhs ? 1 : 0)) + default: + if isPrimitiveForEquality, !other.isPrimitiveForEquality { + return looselyEquals(.string(other.jsString)) + } + if !isPrimitiveForEquality, other.isPrimitiveForEquality { + return OpenUIValue.string(jsString).looselyEquals(other) + } + return false + } + } + + /// JavaScript `===` for the values the store compares (`Object.is` for numbers + /// aside from NaN, structural for composites since they have no identity here). + func isSameValue(_ other: OpenUIValue) -> Bool { + switch (self, other) { + case (.number(let lhs), .number(let rhs)): + if lhs.isNaN && rhs.isNaN { return true } + return lhs == rhs && lhs.sign == rhs.sign + default: + return self == other + } + } +} + +/// lang-core's lenient `toNumber`: numbers pass through, numeric strings parse +/// (anything else becomes 0), booleans become 1/0, everything else is 0. +public func toNumber(_ value: OpenUIValue) -> Double { + switch value { + case .number(let number): return number + case .string(let string): + let number = jsStringToNumber(string) + return number.isNaN ? 0 : number + case .bool(let bool): return bool ? 1 : 0 + default: return 0 + } +} + +// MARK: - Number → String (ECMAScript Number::toString) + +/// Formats a number exactly like JavaScript's `String(number)`. +public func jsNumberToString(_ value: Double) -> String { + if value.isNaN { return "NaN" } + if value == 0 { return "0" } + if value < 0 { return "-" + jsNumberToString(-value) } + if value.isInfinite { return "Infinity" } + + let (digits, n) = shortestDigits(value) + let k = digits.count + if k <= n && n <= 21 { + return digits + String(repeating: "0", count: n - k) + } + if 0 < n && n <= 21 { + let index = digits.index(digits.startIndex, offsetBy: n) + return digits[.. (String, Int) { + // Swift's description is the shortest representation that round-trips. + let description = "\(value)" + var mantissa = Substring(description) + var exponent = 0 + if let e = description.firstIndex(where: { $0 == "e" || $0 == "E" }) { + mantissa = description[.. 1 ? parts[1] : "" + var digits = Array(integerPart + fractionPart) + var point = integerPart.count + exponent + while digits.first == "0" { + digits.removeFirst() + point -= 1 + } + while digits.last == "0" { digits.removeLast() } + return (String(digits), point) +} + +// MARK: - String → Number (ECMAScript StringToNumber) + +let jsWhitespace: Set = [ + "\u{0009}", "\u{000A}", "\u{000B}", "\u{000C}", "\u{000D}", "\u{0020}", "\u{00A0}", "\u{1680}", + "\u{2000}", "\u{2001}", "\u{2002}", "\u{2003}", "\u{2004}", "\u{2005}", "\u{2006}", "\u{2007}", + "\u{2008}", "\u{2009}", "\u{200A}", "\u{2028}", "\u{2029}", "\u{202F}", "\u{205F}", "\u{3000}", + "\u{FEFF}", +] + +/// JavaScript `String.prototype.trim()`. +func jsTrim(_ string: String) -> String { + let scalars = Array(string.unicodeScalars) + var start = 0 + var end = scalars.count + while start < end, jsWhitespace.contains(scalars[start]) { start += 1 } + while end > start, jsWhitespace.contains(scalars[end - 1]) { end -= 1 } + var view = String.UnicodeScalarView() + view.append(contentsOf: scalars[start.. String { + var scalars = Array(string.unicodeScalars) + while let last = scalars.last, jsWhitespace.contains(last) { scalars.removeLast() } + var view = String.UnicodeScalarView() + view.append(contentsOf: scalars) + return String(view) +} + +/// JavaScript `Number(string)`. +public func jsStringToNumber(_ string: String) -> Double { + let text = jsTrim(string) + if text.isEmpty { return 0 } + switch text { + case "Infinity", "+Infinity": return .infinity + case "-Infinity": return -.infinity + default: break + } + let chars = Array(text.utf8) + if chars.count > 2, chars[0] == UInt8(ascii: "0") { + let radix: Int? = + switch chars[1] { + case UInt8(ascii: "x"), UInt8(ascii: "X"): 16 + case UInt8(ascii: "o"), UInt8(ascii: "O"): 8 + case UInt8(ascii: "b"), UInt8(ascii: "B"): 2 + default: nil + } + if let radix { + var result = 0.0 + for char in chars[2...] { + guard let digit = Int(String(UnicodeScalar(char)), radix: radix) else { return .nan } + result = result * Double(radix) + Double(digit) + } + return result + } + } + guard isStrDecimalLiteral(chars) else { return .nan } + return Double(text) ?? .nan +} + +/// `[+-] (digits [. digits?] | . digits) [(e|E) [+-] digits]` +private func isStrDecimalLiteral(_ chars: [UInt8]) -> Bool { + var i = 0 + func isDigit(_ index: Int) -> Bool { + index < chars.count && chars[index] >= UInt8(ascii: "0") && chars[index] <= UInt8(ascii: "9") + } + if i < chars.count, chars[i] == UInt8(ascii: "+") || chars[i] == UInt8(ascii: "-") { i += 1 } + var sawDigits = false + while isDigit(i) { + i += 1 + sawDigits = true + } + if i < chars.count, chars[i] == UInt8(ascii: ".") { + i += 1 + while isDigit(i) { + i += 1 + sawDigits = true + } + } + guard sawDigits else { return false } + if i < chars.count, chars[i] == UInt8(ascii: "e") || chars[i] == UInt8(ascii: "E") { + i += 1 + if i < chars.count, chars[i] == UInt8(ascii: "+") || chars[i] == UInt8(ascii: "-") { i += 1 } + guard isDigit(i) else { return false } + while isDigit(i) { i += 1 } + } + return i == chars.count +} + +extension UInt16 { + /// A UTF-16 code unit for an ASCII scalar; JavaScript strings are UTF-16. + init(ascii scalar: Unicode.Scalar) { + self = UInt16(scalar.value) + } +} diff --git a/packages/swift-lang/Sources/OpenUILang/Lexer.swift b/packages/swift-lang/Sources/OpenUILang/Lexer.swift new file mode 100644 index 0000000000..a8ecf0d323 --- /dev/null +++ b/packages/swift-lang/Sources/OpenUILang/Lexer.swift @@ -0,0 +1,314 @@ +/// Token types for OpenUI Lang (mirrors lang-core's `T` enum). +public enum TokenType: Sendable, Equatable { + case newline + case lParen, rParen, lBrack, rBrack, lBrace, rBrace + case comma, colon, equals + case `true`, `false`, null + case eof + /// Carries a string value. + case str + /// Carries a numeric value. + case num + /// Lowercase identifier: a reference. + case ident + /// PascalCase identifier: a component name or reference. + case type + /// `$identifier`: a reactive state reference. + case stateVar + case dot, plus, minus, star, slash, percent + case eqEq, notEq, greater, less, greaterEq, lessEq + case and, or, not, question + /// `@identifier`: a builtin call. + case builtinCall +} + +public struct Token: Sendable, Equatable { + public var type: TokenType + public var string: String? + public var number: Double? + + init(_ type: TokenType, string: String? = nil, number: Double? = nil) { + self.type = type + self.string = string + self.number = number + } + + /// `String(tok.v)` for identifier-like tokens used as keys. + var valueString: String { + if let string { return string } + if let number { return jsNumberToString(number) } + return "undefined" + } +} + +extension UInt16 { + fileprivate var isASCIIDigit: Bool { self >= 0x30 && self <= 0x39 } + fileprivate var isASCIILower: Bool { self >= 0x61 && self <= 0x7A } + fileprivate var isASCIIUpper: Bool { self >= 0x41 && self <= 0x5A } + fileprivate var isIdentifierStart: Bool { isASCIILower || isASCIIUpper || self == 0x5F } + fileprivate var isIdentifierPart: Bool { isIdentifierStart || isASCIIDigit } +} + +private func char(_ scalar: Unicode.Scalar) -> UInt16 { UInt16(scalar.value) } + +/// Tokenizes OpenUI Lang source into a flat token array ending with `.eof`. +/// +/// Works on UTF-16 code units, like the JavaScript lexer, so string slicing, +/// escapes and skipped characters (emoji outside strings) behave identically. +public func tokenize(_ source: String) -> [Token] { + let src = Array(source.utf16) + let n = src.count + var tokens: [Token] = [] + var i = 0 + + func push(_ type: TokenType) { tokens.append(Token(type)) } + + while i < n { + // Horizontal whitespace; newlines are significant. + while i < n && (src[i] == char(" ") || src[i] == char("\t") || src[i] == char("\r")) { i += 1 } + if i >= n { break } + + let c = src[i] + let next: UInt16? = i + 1 < n ? src[i + 1] : nil + + switch c { + case char("\n"): + push(.newline) + i += 1 + continue + case char("("): + push(.lParen) + i += 1 + continue + case char(")"): + push(.rParen) + i += 1 + continue + case char("["): + push(.lBrack) + i += 1 + continue + case char("]"): + push(.rBrack) + i += 1 + continue + case char("{"): + push(.lBrace) + i += 1 + continue + case char("}"): + push(.rBrace) + i += 1 + continue + case char(","): + push(.comma) + i += 1 + continue + case char(":"): + push(.colon) + i += 1 + continue + case char("="): + if next == char("=") { + push(.eqEq) + i += 2 + } else { + push(.equals) + i += 1 + } + continue + case char("!"): + if next == char("=") { + push(.notEq) + i += 2 + } else { + push(.not) + i += 1 + } + continue + case char(">"): + if next == char("=") { + push(.greaterEq) + i += 2 + } else { + push(.greater) + i += 1 + } + continue + case char("<"): + if next == char("=") { + push(.lessEq) + i += 2 + } else { + push(.less) + i += 1 + } + continue + case char("&"): + // A single `&` is accepted as `&&`. + push(.and) + i += next == char("&") ? 2 : 1 + continue + case char("|"): + // A single `|` is accepted as `||`. + push(.or) + i += next == char("|") ? 2 : 1 + continue + case char("."): + push(.dot) + i += 1 + continue + case char("?"): + push(.question) + i += 1 + continue + case char("+"): + push(.plus) + i += 1 + continue + case char("*"): + push(.star) + i += 1 + continue + case char("/"): + push(.slash) + i += 1 + continue + case char("%"): + push(.percent) + i += 1 + continue + case char("\""): + let start = i + i += 1 + var isClosed = false + while i < n { + if src[i] == char("\\") { + i += 2 + } else if src[i] == char("\"") { + i += 1 + isClosed = true + break + } else { + i += 1 + } + } + i = min(i, n) + let raw = Array(src[start..`. + case record(PropType) + /// A reference to another component in the library. + case component(String) + case union([PropType]) + /// A type shown by name in signatures (like lang-core's `tagSchemaId`), + /// serialized as its underlying schema. + case named(String, PropType) + + /// The action prop type (`ActionExpression`): an `Action([...])` expression, + /// or one of the legacy action objects. + public static let actionExpression: PropType = .named( + "ActionExpression", + .union([ + .object([Prop("type", .literal("open_url")), Prop("url", .string)]), + .object([ + Prop("type", .literal("continue_conversation")), Prop("context", .string, .optional), + ]), + .object([Prop("type", .string), Prop("params", .record(.any), .optional)]), + ])) +} + +/// A component prop: its name, type, and whether it may be omitted. +public struct Prop: Sendable, Equatable { + public enum Presence: Sendable, Equatable { + case required + case optional + /// Filled with this value when missing (`.default(value)` in Zod). + case defaulted(OpenUIValue) + } + + public var name: String + public var type: PropType + public var presence: Presence + /// Binds two-way to a `$state` variable (lang-core's `reactive()`), shown as + /// `$binding` in signatures. + public var isBinding: Bool + public var description: String? + + public init( + _ name: String, _ type: PropType, _ presence: Presence = .required, binding: Bool = false, + description: String? = nil + ) { + self.name = name + self.type = type + self.presence = presence + self.isBinding = binding + self.description = description + } + + /// Optional in signatures: `.optional` and `.defaulted` props may be omitted. + public var isOptional: Bool { presence != .required } + + /// Listed in the JSON Schema's `required`: Zod treats defaulted props as + /// required in its output schema. + var isSchemaRequired: Bool { presence != .optional } + + var defaultValue: OpenUIValue? { + if case .defaulted(let value) = presence { return value } + return nil + } +} + +/// A component's contract with the model: name, description and props in +/// positional order. Independent of how any UI layer renders it. +public struct ComponentSchema: Sendable, Equatable { + public var name: String + public var description: String + public var props: [Prop] + + public init(_ name: String, description: String, props: [Prop]) { + self.name = name + self.description = description + self.props = props + } + + /// The prompt signature, e.g. `TextContent(text: string, size?: "small" | "large")`. + public var signature: String { + let params = props.map { prop in + let type = typeAnnotation(prop.type, binding: prop.isBinding) + return prop.isOptional ? "\(prop.name)?: \(type)" : "\(prop.name): \(type)" + } + return "\(name)(\(params.joined(separator: ", ")))" + } + + /// This component's entry in the library JSON Schema's `$defs`. + public var jsonSchema: OpenUIValue { + var schema = objectSchema(props).objectValue! + if !description.isEmpty { schema["description"] = .string(description) } + return .object(schema) + } +} + +/// A library component: its schema plus how a UI layer renders it. +public struct ComponentDefinition { + public var schema: ComponentSchema + public var content: Content + + public init(_ schema: ComponentSchema, content: Content) { + self.schema = schema + self.content = content + } + + public var name: String { schema.name } +} + +// MARK: - Signatures + +private func typeAnnotation(_ type: PropType, binding: Bool) -> String { + let base = baseTypeAnnotation(type) + return binding ? "$binding<\(base)>" : base +} + +private func baseTypeAnnotation(_ type: PropType) -> String { + switch type { + case .named(let name, _), .component(let name): + return name + case .union(let options): + return options.isEmpty + ? "any" : options.map { typeAnnotation($0, binding: false) }.joined(separator: " | ") + case .array(let inner, _): + let innerType = typeAnnotation(inner, binding: false) + if case .union = inner { return "(\(innerType))[]" } + return "\(innerType)[]" + case .string: return "string" + case .number: return "number" + case .boolean: return "boolean" + case .any: return "any" + case .record(let value): return "Record" + case .enumeration(let values): return values.map { "\"\($0)\"" }.joined(separator: " | ") + case .literal(let value): + if case .string(let string) = value { return "\"\(string)\"" } + return value.jsString + case .object(let props): + let fields = props.map { prop in + "\(prop.name)\(prop.isOptional ? "?" : ""): \(typeAnnotation(prop.type, binding: prop.isBinding))" + } + return "{\(fields.joined(separator: ", "))}" + } +} + +// MARK: - JSON Schema (Zod 4 `toJSONSchema` output) + +func jsonSchema(_ type: PropType) -> OpenUIValue { + switch type { + case .string: return ["type": "string"] + case .number: return ["type": "number"] + case .boolean: return ["type": "boolean"] + case .any: return [:] + case .enumeration(let values): + return ["type": "string", "enum": .array(values.map { .string($0) })] + case .literal(let value): + return ["type": .string(value.jsTypeof), "const": value] + case .array(let inner, let minItems): + // Zod writes length checks before the type. + var schema = OpenUIObject() + if let minItems { schema["minItems"] = .number(Double(minItems)) } + schema["type"] = "array" + schema["items"] = jsonSchema(inner) + return .object(schema) + case .object(let props): return objectSchema(props) + case .record(let value): + return [ + "type": "object", "propertyNames": ["type": "string"], + "additionalProperties": jsonSchema(value), + ] + case .component(let name): return ["$ref": .string("#/$defs/\(name)")] + case .union(let options): return ["anyOf": .array(options.map(jsonSchema))] + case .named(_, let inner): return jsonSchema(inner) + } +} + +private func propSchema(_ prop: Prop) -> OpenUIValue { + var schema = OpenUIObject() + if let defaultValue = prop.defaultValue { schema["default"] = defaultValue } + for (key, value) in jsonSchema(prop.type).objectValue ?? OpenUIObject() { schema[key] = value } + if let description = prop.description { schema["description"] = .string(description) } + return .object(schema) +} + +func objectSchema(_ props: [Prop]) -> OpenUIValue { + var schema: OpenUIObject = ["type": "object"] + schema["properties"] = .object(OpenUIObject(props.map { ($0.name, propSchema($0)) })) + let required = props.filter(\.isSchemaRequired).map { OpenUIValue.string($0.name) } + if !required.isEmpty { schema["required"] = .array(required) } + schema["additionalProperties"] = false + return .object(schema) +} + +// MARK: - Library + +/// A component library: the components a model may use, their prompt, and the +/// schema the parser maps positional arguments with. `Content` is how a UI +/// layer renders each component. +public struct Library: ComponentLibrary { + /// Components in library order (the order of the prompt and the schema). + public let components: [ComponentDefinition] + public let root: String? + public let componentGroups: [ComponentGroup]? + public let id: String? + public let paramMap: ParamMap + private let byName: [String: Int] + private let bindings: Set + + public init( + components: [ComponentDefinition], root: String? = nil, + componentGroups: [ComponentGroup]? = nil, id: String? = nil + ) { + let byName = Dictionary( + components.enumerated().map { ($0.element.name, $0.offset) }, uniquingKeysWith: { $1 }) + if let root, byName[root] == nil { + preconditionFailure( + "[createLibrary] Root component \"\(root)\" was not found in components. Available components: \(components.map(\.name).joined(separator: ", "))" + ) + } + self.components = components + self.root = root + self.componentGroups = componentGroups + self.id = id + self.byName = byName + self.bindings = Set( + components.flatMap { component in + component.schema.props.filter(\.isBinding).map { "\(component.name).\($0.name)" } + }) + self.paramMap = compileSchema(Self.buildSchema(components)) + } + + public var rootName: String? { root } + + public var componentNames: [String] { components.map(\.name) } + + public func component(named name: String) -> ComponentDefinition? { + byName[name].map { components[$0] } + } + + public func isReactiveProp(_ component: String, _ prop: String) -> Bool { + bindings.contains("\(component).\(prop)") + } + + /// The library JSON Schema, shaped like lang-core's `library.toJSONSchema()`. + public func toJSONSchema() -> OpenUIValue { + Self.buildSchema(components) + } + + private static func buildSchema(_ components: [ComponentDefinition]) -> OpenUIValue { + var properties = OpenUIObject() + for component in components { + properties[component.name] = ["$ref": .string("#/$defs/\(component.name)")] + } + var defs = OpenUIObject() + for name in definitionOrder(components) { + defs[name] = components.first { $0.name == name }!.schema.jsonSchema + } + var root: OpenUIObject = ["$schema": "https://json-schema.org/draft/2020-12/schema"] + root["type"] = "object" + root["properties"] = .object(properties) + root["required"] = .array(components.map { .string($0.name) }) + root["additionalProperties"] = false + root["$defs"] = .object(defs) + return .object(root) + } + + /// Zod adds a component to `$defs` the first time its depth-first walk of the + /// library reaches it, so a component referenced by an earlier one comes + /// before its own place in the list. + private static func definitionOrder(_ components: [ComponentDefinition]) -> [String] { + var order: [String] = [] + var seen: Set = [] + + func visit(_ name: String) { + guard !seen.contains(name), let component = components.first(where: { $0.name == name }) + else { return } + seen.insert(name) + order.append(name) + for prop in component.schema.props { walk(prop.type) } + } + + func walk(_ type: PropType) { + switch type { + case .component(let name): visit(name) + case .array(let element, _): walk(element) + case .record(let value): walk(value) + case .named(_, let underlying): walk(underlying) + case .object(let props): for prop in props { walk(prop.type) } + case .union(let options): for option in options { walk(option) } + case .string, .number, .boolean, .any, .enumeration, .literal: break + } + } + + for component in components { visit(component.name) } + return order + } + + /// The prompt inputs for this library. + public func promptSpec(_ options: PromptOptions = PromptOptions()) -> PromptSpec { + PromptSpec( + root: root, + components: components.map { + ( + $0.name, + ComponentPromptSpec(signature: $0.schema.signature, description: $0.schema.description) + ) + }, componentGroups: componentGroups, options: options) + } + + /// The system prompt for this library. + public func prompt(_ options: PromptOptions = PromptOptions()) -> String { + generatePrompt(promptSpec(options)) + } + + /// A JSON `LibrarySpec` (root, component signatures, groups, schema), the + /// same shape as lang-core's `library.toSpec()`. A server can pass it to + /// `generateSystemPrompt({ library })` to build the prompt for this app. + public func toSpec() -> OpenUIValue { + var spec = OpenUIObject() + spec["root"] = root.map { .string($0) } ?? .undefined + spec["components"] = .object( + OpenUIObject( + components.map { component in + var entry: OpenUIObject = ["signature": .string(component.schema.signature)] + entry["description"] = .string(component.schema.description) + return (component.name, .object(entry)) + })) + if let componentGroups { + spec["componentGroups"] = .array( + componentGroups.map { group in + var entry: OpenUIObject = ["name": .string(group.name)] + entry["components"] = .array(group.components.map { .string($0) }) + if let notes = group.notes { entry["notes"] = .array(notes.map { .string($0) }) } + return .object(entry) + }) + } + spec["schema"] = toJSONSchema() + if let id { spec["id"] = .string(id) } + return .object(spec) + } +} + +// Libraries are usually global constants; with sendable content (SwiftUI's +// main-actor render closures are) they can be shared across concurrency domains. +extension ComponentDefinition: Sendable where Content: Sendable {} +extension Library: Sendable where Content: Sendable {} diff --git a/packages/swift-lang/Sources/OpenUILang/Materialize.swift b/packages/swift-lang/Sources/OpenUILang/Materialize.swift new file mode 100644 index 0000000000..c54fd1f751 --- /dev/null +++ b/packages/swift-lang/Sources/OpenUILang/Materialize.swift @@ -0,0 +1,266 @@ +/// Whether a prop value still holds AST that needs runtime evaluation. +func containsDynamicValue(_ value: OpenUIValue) -> Bool { + switch value { + case .ast, .reactiveAssign: return true + case .array(let items): return items.contains(where: containsDynamicValue) + case .element(let element): return element.props.values.contains(where: containsDynamicValue) + case .object(let object): return object.values.contains(where: containsDynamicValue) + case .actionPlan(let plan): + return plan.steps.contains { if case .set = $0 { true } else { false } } + case .actionStep(.set): return true + default: return false + } +} + +private enum RefResolution { + case unresolved + case runtimeRef(ASTNode) + case target(ASTNode) +} + +/// Shared ref handling: cycle and missing-symbol detection, and Query/Mutation +/// declarations becoming `RuntimeRef`s. +private func lookupRef(_ name: String, _ ctx: MaterializeContext) -> RefResolution { + guard !ctx.visited.contains(name), let target = ctx.syms[name] else { + ctx.unres.append(name) + return .unresolved + } + ctx.markReached(name) + if case .comp(let callee, _, _) = target, isReservedCall(callee) { + let refType: RuntimeRefType = callee == ReservedCall.mutation ? .mutation : .query + return .runtimeRef(.runtimeRef(name, refType: refType)) + } + return .target(target) +} + +private func withStatement(_ name: String, _ ctx: MaterializeContext, _ body: () -> T) -> T { + ctx.visited.insert(name) + let previous = ctx.currentStatementId + ctx.currentStatementId = name + defer { + ctx.currentStatementId = previous + ctx.visited.remove(name) + } + return body() +} + +private func resolveRefValue(_ name: String, _ ctx: MaterializeContext) -> OpenUIValue { + switch lookupRef(name, ctx) { + case .unresolved: return .null + case .runtimeRef(let node): return .ast(node) + case .target(let target): + return withStatement(name, ctx) { + var result = materializeValue(target, ctx) + if case .element(var element) = result { + element.statementId = name + result = .element(element) + } + return result + } + } +} + +private func resolveRefExpr(_ name: String, _ ctx: MaterializeContext) -> ASTNode { + switch lookupRef(name, ctx) { + case .unresolved: return .ph(name) + case .runtimeRef(let node): return node + case .target(let target): return withStatement(name, ctx) { materializeExpr(target, ctx) } + } +} + +/// For a lazy builtin such as `Each(arr, varName, template)`, scopes the loop +/// variable so template refs to it are left alone. `nil` when not applicable. +private func materializeLazyBuiltin( + _ name: String, _ args: [ASTNode], _ mappedProps: [ASTEntry]?, _ ctx: MaterializeContext, + _ scopedRefs: Set +) -> ASTNode? { + guard lazyBuiltins.contains(name), args.count >= 3 else { return nil } + let varName: String + switch args[1] { + case .ref(let n): varName = n + case .str(let v): varName = v + default: return nil + } + var nextScoped = scopedRefs + nextScoped.insert(varName) + let recursed = args.enumerated().map { index, arg in + index == 1 ? arg : materializeExprInternal(arg, ctx, nextScoped) + } + return .comp(name: name, args: recursed, mappedProps: mappedProps) +} + +private func materializeExprInternal( + _ node: ASTNode, _ ctx: MaterializeContext, _ scopedRefs: Set +) -> ASTNode { + switch node { + case .ref(let name): + return scopedRefs.contains(name) ? node : resolveRefExpr(name, ctx) + case .ph: + return node + case .comp(let name, let args, let mappedProps): + if let lazy = materializeLazyBuiltin(name, args, mappedProps, ctx, scopedRefs) { return lazy } + let recursed = args.map { materializeExprInternal($0, ctx, scopedRefs) } + if isBuiltin(name) || isReservedCall(name) { + return .comp(name: name, args: recursed, mappedProps: mappedProps) + } + if let params = ctx.cat?[name] { + // Catalog component: mapped props let the evaluator build an element. + let mapped = zip(params, recursed).map { ASTEntry($0.name, $1) } + return .comp(name: name, args: recursed, mappedProps: mapped) + } + pushValidationIssue(ctx, name, "", .unknownComponent(available: ctx.cat?.componentNames)) + return .comp(name: name, args: recursed, mappedProps: mappedProps) + case .arr(let elements): + return .arr(elements.map { materializeExprInternal($0, ctx, scopedRefs) }) + case .obj(let entries): + return .obj( + entries.map { ASTEntry($0.key, materializeExprInternal($0.value, ctx, scopedRefs)) }) + case .binOp(let op, let left, let right): + return .binOp( + op: op, left: materializeExprInternal(left, ctx, scopedRefs), + right: materializeExprInternal(right, ctx, scopedRefs)) + case .unaryOp(let op, let operand): + return .unaryOp(op: op, operand: materializeExprInternal(operand, ctx, scopedRefs)) + case .ternary(let cond, let then, let otherwise): + return .ternary( + cond: materializeExprInternal(cond, ctx, scopedRefs), + then: materializeExprInternal(then, ctx, scopedRefs), + else: materializeExprInternal(otherwise, ctx, scopedRefs)) + case .member(let obj, let field): + return .member(obj: materializeExprInternal(obj, ctx, scopedRefs), field: field) + case .index(let obj, let index): + return .index( + obj: materializeExprInternal(obj, ctx, scopedRefs), + index: materializeExprInternal(index, ctx, scopedRefs)) + case .assign(let target, let value): + return .assign(target: target, value: materializeExprInternal(value, ctx, scopedRefs)) + default: + // Literals, StateRef and RuntimeRef pass through unchanged. + return node + } +} + +/// Normalizes an AST node for use inside a runtime expression: resolves refs and +/// adds `mappedProps` to catalog components. The structure stays AST. +func materializeExpr(_ node: ASTNode, _ ctx: MaterializeContext) -> ASTNode { + materializeExprInternal(node, ctx, []) +} + +/// Schema-aware lowering in one pass: resolves refs, maps positional arguments +/// to named props, validates required props, applies defaults, turns literals +/// into plain values and keeps runtime expressions as AST. +/// +/// Returns an element for catalog components, AST for builtins and runtime +/// expressions, plain values for literals and collections, and `null` for +/// placeholders and dropped components. +func materializeValue(_ node: ASTNode, _ ctx: MaterializeContext) -> OpenUIValue { + switch node { + case .ref(let name): + return resolveRefValue(name, ctx) + case .str(let value): + return .string(value) + case .num(let value): + return .number(value) + case .bool(let value): + return .bool(value) + case .null, .ph: + return .null + case .arr(let elements): + var items: [OpenUIValue] = [] + for element in elements { + // Unresolved placeholders are dropped from arrays, and so are components + // or refs that resolved to null (incomplete, unresolved or unknown). + if case .ph = element { continue } + let value = materializeValue(element, ctx) + if value == .null { + switch element { + case .comp, .ref: continue + default: break + } + } + items.append(value) + } + return .array(items) + case .obj(let entries): + var object = OpenUIObject() + for entry in entries { object[entry.key] = materializeValue(entry.value, ctx) } + return .object(object) + case .comp(let name, let args, let mappedProps): + return materializeComponent(name, args, mappedProps, ctx) + default: + return node.isRuntimeExpr ? .ast(materializeExpr(node, ctx)) : .ast(node) + } +} + +private func materializeComponent( + _ name: String, _ args: [ASTNode], _ mappedProps: [ASTEntry]?, _ ctx: MaterializeContext +) -> OpenUIValue { + // Builtins (Sum, Count, Action, ...) stay AST for the runtime. + if isBuiltin(name) { + if let lazy = materializeLazyBuiltin(name, args, mappedProps, ctx, []) { return .ast(lazy) } + return .ast( + .comp(name: name, args: args.map { materializeExpr($0, ctx) }, mappedProps: mappedProps)) + } + // Query/Mutation are only valid as top-level statements. + if isReservedCall(name) { + pushValidationIssue(ctx, name, "", .inlineReserved) + return .null + } + guard let params = ctx.cat?[name] else { + pushValidationIssue(ctx, name, "", .unknownComponent(available: ctx.cat?.componentNames)) + return .null + } + + var props = OpenUIObject() + // Set when a required prop holds invalid data with no default to fall back on. + var dropComponent = false + for (param, arg) in zip(params, args) { + var value = materializeValue(arg, ctx) + let invalid = + param.schema != nil + && validateSchemaValue(&value, param.schema, name, "/\(param.name)", ctx) + props[param.name] = value + if invalid, + resolveInvalidValue( + &props, param.name, required: param.required, defaultValue: param.defaultValue) + { + dropComponent = true + } + } + + // Extra positional arguments are dropped. + if args.count > params.count { + pushValidationIssue(ctx, name, "", .excessArgs(declared: params.count, got: args.count)) + } + + // Required props: fall back to the schema default before dropping the component. + let missingRequired = params.filter { param in + param.required && (!props.contains(param.name) || props[param.name] == .null) + } + if !missingRequired.isEmpty { + let stillInvalid = missingRequired.filter { param in + if let defaultValue = param.defaultValue { + props[param.name] = defaultValue + return false + } + return true + } + if !stillInvalid.isEmpty { + let signature = buildParamsSignature(name, params) + for param in stillInvalid { + pushValidationIssue( + ctx, name, "/\(param.name)", + props.contains(param.name) + ? .nullRequired(signature: signature) : .missingRequired(signature: signature)) + } + return .null + } + } + + if dropComponent { return .null } + + let hasDynamicProps = props.values.contains(where: containsDynamicValue) + return .element( + ElementNode( + typeName: name, props: props, partial: ctx.partial, hasDynamicProps: hasDynamicProps)) +} diff --git a/packages/swift-lang/Sources/OpenUILang/Merge.swift b/packages/swift-lang/Sources/OpenUILang/Merge.swift new file mode 100644 index 0000000000..50023d6d1d --- /dev/null +++ b/packages/swift-lang/Sources/OpenUILang/Merge.swift @@ -0,0 +1,136 @@ +/// Edit mode: applies a patch program to an existing one. Port of lang-core's +/// `parser/merge.ts`. + +private struct ParsedStatement { + var id: String + var ast: ASTNode + var raw: String +} + +/// Splits source into top-level statements at newlines outside brackets and +/// strings. Indexes UTF-16 code units, like the JavaScript it mirrors. +private func splitStatementSource(_ input: String) -> [String] { + let units = Array(input.utf16) + var statements: [String] = [] + var depth = 0 + var quote: UInt16? + var escaped = false + var start = 0 + + func appendTrimmed(_ range: Range) { + let statement = jsTrim(String(decoding: units[range], as: UTF16.self)) + if !statement.isEmpty { statements.append(statement) } + } + + for (i, c) in units.enumerated() { + if escaped { + escaped = false + continue + } + if let open = quote { + if c == UInt16(ascii: "\\") { + escaped = true + } else if c == open { + quote = nil + } + continue + } + switch c { + case UInt16(ascii: "\""), UInt16(ascii: "'"): + quote = c + case UInt16(ascii: "("), UInt16(ascii: "["), UInt16(ascii: "{"): + depth += 1 + case UInt16(ascii: ")"), UInt16(ascii: "]"), UInt16(ascii: "}"): + depth = max(0, depth - 1) + case UInt16(ascii: "\n") where depth <= 0: + appendTrimmed(start.. [ParsedStatement] { + let trimmed = jsTrim(input) + if trimmed.isEmpty { return [] } + return splitStatementSource(trimmed).compactMap { raw in + guard let statement = splitStatements(tokenize(raw)).first else { return nil } + return ParsedStatement(id: statement.id, ast: parseExpression(statement.tokens), raw: raw) + } +} + +/// Removes statements unreachable from the root. `$state` declarations are +/// always kept: they're read at runtime, not through references. +private func removeUnreachable( + _ order: inout [String], _ merged: inout [String: String], _ asts: [String: ASTNode], + rootId: String +) { + guard asts[rootId] != nil else { return } + + var reachable: Set = [rootId] + var queue = [rootId] + while let id = queue.popLast() { + asts[id]?.walk { node in + let name: String + switch node { + case .ref(let ref): name = ref + case .runtimeRef(let ref, _): name = ref + default: return + } + if !reachable.contains(name), asts[name] != nil { + reachable.insert(name) + queue.append(name) + } + } + } + for id in order where id.hasPrefix("$") { reachable.insert(id) } + + for id in order where !reachable.contains(id) { merged[id] = nil } + order.removeAll { !reachable.contains($0) } +} + +/// Merges a patch into an existing program. Patch statements replace existing +/// ones with the same name, `name = null` deletes a statement, and statements +/// no longer reachable from `rootId` are dropped. +public func mergeStatements(_ existing: String, _ patch: String, rootId: String = "root") + -> String +{ + let existingStatements = parseStatements(existing) + let patchStatements = parseStatements(stripFences(patch)) + + if existingStatements.isEmpty { + return patchStatements.map(\.raw).joined(separator: "\n") + } + if patchStatements.isEmpty { return existing } + + var merged: [String: String] = [:] + var asts: [String: ASTNode] = [:] + var order: [String] = [] + + // A repeated name stays in `order` twice, as in lang-core; both copies print + // the last definition. + for statement in existingStatements { + order.append(statement.id) + merged[statement.id] = statement.raw + asts[statement.id] = statement.ast + } + + for statement in patchStatements { + if statement.ast == .null { + merged[statement.id] = nil + asts[statement.id] = nil + if let index = order.firstIndex(of: statement.id) { order.remove(at: index) } + continue + } + if merged[statement.id] == nil { order.append(statement.id) } + merged[statement.id] = statement.raw + asts[statement.id] = statement.ast + } + + removeUnreachable(&order, &merged, asts, rootId: rootId) + + return order.compactMap { merged[$0] }.joined(separator: "\n") +} diff --git a/packages/swift-lang/Sources/OpenUILang/Observability.swift b/packages/swift-lang/Sources/OpenUILang/Observability.swift new file mode 100644 index 0000000000..3eeea6fa05 --- /dev/null +++ b/packages/swift-lang/Sources/OpenUILang/Observability.swift @@ -0,0 +1,151 @@ +import Foundation + +/// How serious an `ObservabilityEvent` is. +public enum ObservabilityLevel: String, Sendable { + case info, warning, error +} + +/// One event on the observability bus. +public struct ObservabilityEvent: Sendable { + public var level: ObservabilityLevel + /// Milliseconds since 1970, like JavaScript's `Date.now()`. + public var timestamp: Double + /// What happened: `kind` names it, the other fields describe it. + public var detail: OpenUIObject +} + +/// Port of `@openuidev/observability`: one shared bus that OpenUI emits events +/// on and sinks (logging, analytics, a debugging view) listen to. +/// +/// The runtime emits a `react-lang:stream` event as each response streams and +/// when it settles, with the same fields as react-lang's, so a sink written +/// for the web reads these too. Listeners are called on the emitting thread. +public final class Observability: @unchecked Sendable { + public typealias Handler = @Sendable (ObservabilityEvent) -> Void + + /// The bus OpenUI emits on. + public static let shared = Observability() + + private let lock = NSLock() + private var listeners: [(id: Int, key: String, handler: Handler)] = [] + private var nextID = 0 + + public init() {} + + /// Whether anything listens, so emitters can skip building events. + public var hasListeners: Bool { lock.withLock { !listeners.isEmpty } } + + /// Milliseconds since 1970, like `Date.now()`. + static func now() -> Double { (Date().timeIntervalSince1970 * 1000).rounded(.down) } + + /// Emits `detail` at `level` to its listeners and to `listenAll` ones. + public func emit(_ level: ObservabilityLevel, _ detail: OpenUIObject) { + let event = ObservabilityEvent(level: level, timestamp: Self.now(), detail: detail) + let targets = lock.withLock { + listeners.filter { $0.key == level.rawValue } + listeners.filter { $0.key == Self.all } + } + for target in targets { target.handler(event) } + } + + public func info(_ detail: OpenUIObject) { emit(.info, detail) } + public func warn(_ detail: OpenUIObject) { emit(.warning, detail) } + public func error(_ detail: OpenUIObject) { emit(.error, detail) } + + /// Listens to every event at `levels`. Call the result to stop. + @discardableResult + public func listen( + _ levels: [ObservabilityLevel], _ handler: @escaping Handler + ) -> @Sendable () -> Void { + let removers = levels.map { subscribe($0.rawValue, handler) } + return { + for remove in removers { remove() } + } + } + + /// Listens to every event. Call the result to stop. + @discardableResult + public func listenAll(_ handler: @escaping Handler) -> @Sendable () -> Void { + subscribe(Self.all, handler) + } + + private static let all = "all" + + private func subscribe(_ key: String, _ handler: @escaping Handler) -> @Sendable () -> Void { + let id = lock.withLock { + nextID += 1 + listeners.append((nextID, key, handler)) + return nextID + } + return { [weak self] in + guard let self else { return } + self.lock.withLock { self.listeners.removeAll { $0.id == id } } + } + } +} + +/// The `kind` of the stream lifecycle events, react-lang's `STREAM_EVENT_KIND`. +public let streamEventKind = "react-lang:stream" + +/// One response's stream lifecycle as react-lang's Renderer publishes it +/// (`advanceStreamingObservability`): a stable id from the first streamed +/// chunk, a `streaming` update for each new chunk, then one `settled` update, +/// repeated only when the errors change. Streaming again starts a new id. +struct StreamObservation: Sendable { + enum Phase: String, Sendable { + case streaming, settled + } + + struct Update: Equatable, Sendable { + var id: String + var phase: Phase + var updateIndex: Int + } + + var id: String? + var updateIndex = 0 + var lastResponse: String? + var hasPublishedStreamingSnapshot = false + var settled = false + var lastSettledErrorKey: String? + var startedAt: Double? + var durationMs: Double? + + /// The update to publish for this state, if any. `settledErrorKey` + /// identifies the errors once settled. + mutating func advance( + isStreaming: Bool, response: String?, settledErrorKey: String?, + idFactory: () -> String = { UUID().uuidString.lowercased() } + ) -> Update? { + if isStreaming { + if settled { self = StreamObservation() } + let id = self.id ?? idFactory() + self.id = id + if hasPublishedStreamingSnapshot && lastResponse == response { return nil } + hasPublishedStreamingSnapshot = true + lastResponse = response + updateIndex += 1 + return Update(id: id, phase: .streaming, updateIndex: updateIndex) + } + guard let id else { return nil } + if settled && (settledErrorKey == nil || lastSettledErrorKey == settledErrorKey) { + return nil + } + updateIndex += 1 + settled = true + lastSettledErrorKey = settledErrorKey + return Update(id: id, phase: .settled, updateIndex: updateIndex) + } + + /// react-lang's `captureStreamTiming`: when the stream started, how long it + /// has run, and its duration, frozen once it settles. + mutating func timing(now: Double = Observability.now()) -> OpenUIObject { + let startedAt = self.startedAt ?? now + self.startedAt = startedAt + if settled, durationMs == nil { durationMs = max(0, now - startedAt) } + var fields: OpenUIObject = [ + "startedAt": .number(startedAt), "elapsedMs": .number(durationMs ?? max(0, now - startedAt)), + ] + if let durationMs { fields["durationMs"] = .number(durationMs) } + return fields + } +} diff --git a/packages/swift-lang/Sources/OpenUILang/OpenUIComponents.swift b/packages/swift-lang/Sources/OpenUILang/OpenUIComponents.swift new file mode 100644 index 0000000000..3421b2afa3 --- /dev/null +++ b/packages/swift-lang/Sources/OpenUILang/OpenUIComponents.swift @@ -0,0 +1,385 @@ +// Generated by Scripts/generate-chat-components.mjs from openuiLibrary in +// @openuidev/react-ui. Don't edit by hand. +// swift-format-ignore-file: LineLength + +/// Schemas for react-ui's general OpenUI library, `openuiLibrary`: the chat +/// library's components without the chat-only follow-ups and sections, plus +/// `Stack` (the root) and `Modal`, with `Card` taking Stack's layout props. +/// Components both libraries define the same way are `ChatComponents`' +/// schemas. +public enum OpenUIComponents { + public static let card = ComponentSchema( + "Card", + description: + "Styled container. variant: \"card\" (default, elevated) | \"sunk\" (recessed) | \"clear\" (transparent). Always full width. Accepts all Stack flex params (default: direction \"column\"). Cards flex to share space in row/wrap layouts.", + props: [ + Prop( + "children", + .array( + .union([ + .component("TextContent"), .component("MarkDownRenderer"), .component("CardHeader"), + .component("Callout"), .component("TextCallout"), .component("CodeBlock"), + .component("Image"), .component("ImageBlock"), .component("ImageGallery"), + .component("Separator"), .component("HorizontalBarChart"), .component("RadarChart"), + .component("PieChart"), .component("RadialChart"), .component("SingleStackedBarChart"), + .component("ScatterChart"), .component("AreaChart"), .component("BarChart"), + .component("LineChart"), .component("Table"), .component("TagBlock"), + .component("Form"), .component("Buttons"), .component("IconButton"), + .component("Steps"), .component("InlineHeader"), .component("EntityList"), + .component("EditableTable"), .component("SnippetCardBlock"), + .component("OverviewCardBlock"), .component("ContextCardBlock"), + .component("CompositeCardBlock"), .component("VisualCardBlock"), .component("Tabs"), + .component("Carousel"), .component("Stack"), + ]))), Prop("variant", .enumeration(["card", "sunk", "clear"]), .optional), + Prop("direction", .enumeration(["row", "column"]), .optional), + Prop("gap", .enumeration(["none", "xs", "s", "m", "l", "xl", "2xl"]), .optional), + Prop("align", .enumeration(["start", "center", "end", "stretch", "baseline"]), .optional), + Prop( + "justify", .enumeration(["start", "center", "end", "between", "around", "evenly"]), + .optional), Prop("wrap", .boolean, .optional), + ]) + + public static let stack = ComponentSchema( + "Stack", + description: + "Flex container. direction: \"row\"|\"column\" (default \"column\"). gap: \"none\"|\"xs\"|\"s\"|\"m\"|\"l\"|\"xl\"|\"2xl\" (default \"m\"). align: \"start\"|\"center\"|\"end\"|\"stretch\"|\"baseline\". justify: \"start\"|\"center\"|\"end\"|\"between\"|\"around\"|\"evenly\".", + props: [ + Prop("children", .array(.any)), + Prop("direction", .enumeration(["row", "column"]), .optional), + Prop("gap", .enumeration(["none", "xs", "s", "m", "l", "xl", "2xl"]), .optional), + Prop("align", .enumeration(["start", "center", "end", "stretch", "baseline"]), .optional), + Prop( + "justify", .enumeration(["start", "center", "end", "between", "around", "evenly"]), + .optional), Prop("wrap", .boolean, .optional), + ]) + + public static let tabItem = ComponentSchema( + "TabItem", + description: "value is unique id, trigger is tab label, content is array of components", + props: [ + Prop("value", .string), Prop("trigger", .string), + Prop( + "content", + .array( + .union([ + .component("TextContent"), .component("MarkDownRenderer"), .component("CardHeader"), + .component("Callout"), .component("TextCallout"), .component("CodeBlock"), + .component("Image"), .component("ImageBlock"), .component("ImageGallery"), + .component("Separator"), .component("HorizontalBarChart"), .component("RadarChart"), + .component("PieChart"), .component("RadialChart"), .component("SingleStackedBarChart"), + .component("ScatterChart"), .component("AreaChart"), .component("BarChart"), + .component("LineChart"), .component("Table"), .component("TagBlock"), + .component("Form"), .component("Buttons"), .component("IconButton"), + .component("Steps"), .component("InlineHeader"), .component("EntityList"), + .component("EditableTable"), .component("SnippetCardBlock"), + .component("OverviewCardBlock"), .component("ContextCardBlock"), + .component("CompositeCardBlock"), .component("VisualCardBlock"), + ]))), + ]) + + public static let accordionItem = ComponentSchema( + "AccordionItem", description: "value is unique id, trigger is section title", + props: [ + Prop("value", .string), Prop("trigger", .string), + Prop( + "content", + .array( + .union([ + .component("TextContent"), .component("MarkDownRenderer"), .component("CardHeader"), + .component("Callout"), .component("TextCallout"), .component("CodeBlock"), + .component("Image"), .component("ImageBlock"), .component("ImageGallery"), + .component("Separator"), .component("HorizontalBarChart"), .component("RadarChart"), + .component("PieChart"), .component("RadialChart"), .component("SingleStackedBarChart"), + .component("ScatterChart"), .component("AreaChart"), .component("BarChart"), + .component("LineChart"), .component("Table"), .component("TagBlock"), + .component("Form"), .component("Buttons"), .component("IconButton"), + .component("Steps"), .component("InlineHeader"), .component("EntityList"), + .component("EditableTable"), .component("SnippetCardBlock"), + .component("OverviewCardBlock"), .component("ContextCardBlock"), + .component("CompositeCardBlock"), .component("VisualCardBlock"), + ]))), + ]) + + public static let carousel = ComponentSchema( + "Carousel", description: "Horizontal scrollable carousel", + props: [ + Prop( + "children", + .array( + .array( + .union([ + .component("TextContent"), .component("MarkDownRenderer"), .component("CardHeader"), + .component("Callout"), .component("TextCallout"), .component("CodeBlock"), + .component("Image"), .component("ImageBlock"), .component("ImageGallery"), + .component("Separator"), .component("HorizontalBarChart"), .component("RadarChart"), + .component("PieChart"), .component("RadialChart"), + .component("SingleStackedBarChart"), .component("ScatterChart"), + .component("AreaChart"), .component("BarChart"), .component("LineChart"), + .component("Table"), .component("TagBlock"), .component("Form"), + .component("Buttons"), .component("IconButton"), .component("Steps"), + .component("InlineHeader"), .component("EntityList"), .component("EditableTable"), + .component("SnippetCardBlock"), .component("OverviewCardBlock"), + .component("ContextCardBlock"), .component("CompositeCardBlock"), + .component("VisualCardBlock"), + ])))), Prop("variant", .enumeration(["card", "sunk"]), .optional), + ]) + + public static let modal = ComponentSchema( + "Modal", + description: + "Modal dialog. open is a reactive $boolean binding — set to true to open, X/Escape/backdrop auto-closes. Put Form with buttons inside children.", + props: [ + Prop("title", .string), Prop("open", .boolean, .optional, binding: true), + Prop( + "children", + .array( + .union([ + .component("TextContent"), .component("MarkDownRenderer"), .component("CardHeader"), + .component("Callout"), .component("TextCallout"), .component("CodeBlock"), + .component("Image"), .component("ImageBlock"), .component("ImageGallery"), + .component("Separator"), .component("HorizontalBarChart"), .component("RadarChart"), + .component("PieChart"), .component("RadialChart"), .component("SingleStackedBarChart"), + .component("ScatterChart"), .component("AreaChart"), .component("BarChart"), + .component("LineChart"), .component("Table"), .component("TagBlock"), + .component("Form"), .component("Buttons"), .component("IconButton"), + .component("Steps"), .component("InlineHeader"), .component("EntityList"), + .component("EditableTable"), .component("SnippetCardBlock"), + .component("OverviewCardBlock"), .component("ContextCardBlock"), + .component("CompositeCardBlock"), .component("VisualCardBlock"), + ]))), Prop("size", .enumeration(["sm", "md", "lg"]), .optional), + ]) + + /// Prompt groups and notes, as in openuiLibrary. + public static let groups: [ComponentGroup] = [ + ComponentGroup( + name: "Layout", + components: [ + "Stack", "Tabs", "TabItem", "Accordion", "AccordionItem", "Steps", "StepsItem", "Carousel", + "Separator", "Modal", + ], + notes: [ + "- For grid-like layouts, use Stack with direction \"row\" and wrap set to true.", + "- Prefer justify \"start\" (or omit justify) with wrap=true for stable columns instead of uneven gutters.", + "- Use nested Stacks when you need explicit rows/sections.", + "- Show/hide sections: $editId != \"\" ? Card([editForm]) : null", + "- Modal: Modal(\"Title\", $showModal, [content]) — $showModal is boolean, X/Escape auto-closes. Put Form with its own buttons inside children.", + "- Use Tabs for alternative views (chart types, data sections) — no $variable needed", + "- Shared filter across Tabs: same $days binding in Query args works across all TabItems", + ]), + ComponentGroup( + name: "Content", + components: [ + "Card", "CardHeader", "TextContent", "MarkDownRenderer", "Callout", "TextCallout", "Image", + "ImageBlock", "ImageGallery", "CodeBlock", "InlineHeader", + ], + notes: [ + "- InlineHeader is a compact heading + description pair for labelling a block (lighter than CardHeader).", + "- Use Cards to group related KPIs or sections. Stack with direction \"row\" for side-by-side layouts.", + "- Success toast: Callout(\"success\", \"Saved\", \"Done.\", $showSuccess) — use @Set($showSuccess, true) in save action, auto-dismisses after 3s. For errors: result.status == \"error\" ? Callout(\"error\", \"Failed\", result.error) : null", + "- KPI card: Card([TextContent(\"Label\", \"small\"), TextContent(\"\" + @Count(@Filter(data.rows, \"field\", \"==\", \"value\")), \"large-heavy\")])", + ]), + ComponentGroup( + name: "Tables", components: ["Table", "Col", "EditableTable"], + notes: [ + "- EditableTable lets the user edit cells inline. Give it a unique name, columns of { type, key, header } with type one of text | number | date-single | select | url (select also needs options: [{ value, label }]).", + "- data is an array of { id, values } rows where values are ordered positionally to match columns. Edited data is submitted when the user clicks Save Changes.", + "- Table is COLUMN-oriented: Table([Col(\"Label\", dataArray), Col(\"Count\", countArray, \"number\")]). Use array pluck for data: data.rows.fieldName", + "- Col data can be component arrays for styled cells: Col(\"Status\", @Each(data.rows, \"item\", Tag(item.status, null, \"sm\", item.status == \"open\" ? \"success\" : \"danger\")))", + "- Row actions: Col(\"Actions\", @Each(data.rows, \"t\", Button(\"Edit\", Action([@Set($showEdit, true), @Set($editId, t.id)]))))", + "- Sortable: sorted = @Sort(data.rows, $sortField, \"desc\"). Bind $sortField to Select. Use sorted.fieldName for Col data", + "- Searchable: filtered = @Filter(data.rows, \"title\", \"contains\", $search). Bind $search to Input", + "- Chain sort + filter: filtered = @Filter(...) then sorted = @Sort(filtered, ...) — use sorted for both Table and Charts", + "- Empty state: @Count(data.rows) > 0 ? Table([...]) : TextContent(\"No data yet\")", + ]), + ComponentGroup( + name: "Charts (2D)", + components: [ + "BarChart", "LineChart", "AreaChart", "RadarChart", "HorizontalBarChart", "Series", + ], + notes: [ + "- Charts accept column arrays: LineChart(labels, [Series(\"Name\", values)]). Use array pluck: LineChart(data.rows.day, [Series(\"Views\", data.rows.views)])", + "- Use Cards to wrap charts with CardHeader for titled sections", + "- Chart + Table from same source: use @Sort or @Filter result for both LineChart and Table Col data", + "- Multiple chart views: use Tabs — Tabs([TabItem(\"line\", \"Line\", [LineChart(...)]), TabItem(\"bar\", \"Bar\", [BarChart(...)])])", + ]), + ComponentGroup( + name: "Charts (1D)", + components: ["PieChart", "RadialChart", "SingleStackedBarChart", "Slice"], + notes: [ + "- PieChart and BarChart need NUMBERS, not objects. For list data, use @Count(@Filter(...)) to aggregate:", + "- PieChart from list: `PieChart([\"Low\", \"Med\", \"High\"], [@Count(@Filter(data.rows, \"priority\", \"==\", \"low\")), @Count(@Filter(data.rows, \"priority\", \"==\", \"medium\")), @Count(@Filter(data.rows, \"priority\", \"==\", \"high\"))], \"donut\")`", + "- KPI from count: `TextContent(\"\" + @Count(@Filter(data.rows, \"status\", \"==\", \"open\")), \"large-heavy\")`", + ]), + ComponentGroup( + name: "Charts (Scatter)", components: ["ScatterChart", "ScatterSeries", "Point"]), + ComponentGroup( + name: "Forms", + components: [ + "Form", "FormControl", "Label", "Input", "TextArea", "Select", "SelectItem", "DatePicker", + "Slider", "CheckBoxGroup", "CheckBoxItem", "RadioGroup", "RadioItem", "SwitchGroup", + "SwitchItem", "Chips", "ChipItem", "OptionCards", "OptionCard", + ], + notes: [ + "- Chips: compact single/multiple selection pills. Use ChipItem references for each option.", + "- OptionCards: larger selectable cards with title, subtitle and an optional Icon or Image on top. Use OptionCard references for each option.", + "- For Form fields, define EACH FormControl as its own reference — do NOT inline all controls in one array. This allows progressive field-by-field streaming.", + "- NEVER nest Form inside Form — each Form should be a standalone container.", + "- Form requires explicit buttons. Always pass a Buttons(...) reference as the third Form argument.", + "- rules is an optional object: {required: true, email: true, minLength: 8, maxLength: 100}", + "- Available rules: required, email, min, max, minLength, maxLength, pattern, url, numeric", + "- The renderer shows error messages automatically — do NOT generate error text in the UI", + "- Conditional fields: $country == \"US\" ? stateField : $country == \"UK\" ? postcodeField : addressField", + "- Edit form in Modal: Modal(\"Edit\", $showEdit, [Form(\"edit\", Buttons([saveBtn, cancelBtn]), [fields...])]). Save button should include @Set($showEdit, false) to close modal.", + ]), + ComponentGroup( + name: "Buttons", components: ["Button", "Buttons", "IconButton"], + notes: [ + "- Icon renders a lucide icon by kebab-case name; it is also used as the icon of IconButton, IconText and OptionCard.", + "- Toggle in @Each: @Each(rows, \"t\", Button(t.status == \"open\" ? \"Close\" : \"Reopen\", Action([...])))", + ]), + ComponentGroup( + name: "Data Display", + components: ["TagBlock", "Tag", "Icon", "EntityList", "ListBlock", "ListItem"], + notes: [ + "- Color-mapped Tag: Tag(value, null, \"sm\", value == \"high\" ? \"danger\" : value == \"medium\" ? \"warning\" : \"neutral\")", + "- EntityList is a compact two-column list of { left, right } rows (e.g. name / value). size='default' also supports a header and footer row; size='small' does not.", + "- ListBlock is a numbered or image list of ListItem references. An action on ListItem is optional.", + ]), + ComponentGroup( + name: "Cards", + components: [ + "SnippetCardBlock", "SnippetCardItem", "OverviewCardBlock", "OverviewCardItem", + "ContextCardBlock", "ContextCardItem", "CompositeCardBlock", "CompositeCardItem", + "VisualCardBlock", "VisualCardItem", "Text", "BoldText", "IconText", "ImageText", + "ImageTextLarge", "MetricIndicatorInline", "MetricIndicatorWithStrikethrough", + ], + notes: [ + "- Card blocks lay out 2+ items in a responsive grid (or carousel where supported). Every item in a block must have the same structure.", + "- SnippetCardItem: small card with lhs (IconText | ImageText) and optional rhs (Text | BoldText) — good for key/value facts.", + "- OverviewCardItem: small card with top (IconText | ImageText | Text) and optional bottom MetricIndicatorInline — good for KPIs.", + "- ContextCardItem: medium card with a title (string or Tag), body text and optional background image — good for summaries.", + "- CompositeCardItem: rich card with header, body array (Text, BoldText, MetricIndicatorInline, IconText, Image, charts, ListBlock, TagBlock, EntityList) and footer (price + Button) — good for products/offers.", + "- VisualCardItem: image-first card with a BoldText body and optional Tag.", + "- Text / BoldText / IconText / ImageText / ImageTextLarge / MetricIndicator* are the inline building blocks used INSIDE card items; do not place them directly in the root Card.", + ]), + ] + + /// Every schema, in library order. + public static let all: [ComponentSchema] = [ + card, + ChatComponents.cardHeader, + ChatComponents.textContent, + ChatComponents.markDownRenderer, + ChatComponents.callout, + ChatComponents.textCallout, + ChatComponents.image, + ChatComponents.imageBlock, + ChatComponents.imageGallery, + ChatComponents.codeBlock, + ChatComponents.inlineHeader, + ChatComponents.table, + ChatComponents.col, + ChatComponents.editableTable, + ChatComponents.barChart, + ChatComponents.lineChart, + ChatComponents.areaChart, + ChatComponents.radarChart, + ChatComponents.horizontalBarChart, + ChatComponents.series, + ChatComponents.pieChart, + ChatComponents.radialChart, + ChatComponents.singleStackedBarChart, + ChatComponents.slice, + ChatComponents.scatterChart, + ChatComponents.scatterSeries, + ChatComponents.point, + ChatComponents.form, + ChatComponents.formControl, + ChatComponents.label, + ChatComponents.input, + ChatComponents.textArea, + ChatComponents.select, + ChatComponents.selectItem, + ChatComponents.datePicker, + ChatComponents.slider, + ChatComponents.checkBoxGroup, + ChatComponents.checkBoxItem, + ChatComponents.radioGroup, + ChatComponents.radioItem, + ChatComponents.switchGroup, + ChatComponents.switchItem, + ChatComponents.chipItem, + ChatComponents.chips, + ChatComponents.optionCard, + ChatComponents.optionCards, + ChatComponents.button, + ChatComponents.buttons, + ChatComponents.iconButton, + stack, + ChatComponents.tabs, + tabItem, + ChatComponents.accordion, + accordionItem, + ChatComponents.steps, + ChatComponents.stepsItem, + carousel, + ChatComponents.separator, + ChatComponents.tagBlock, + ChatComponents.tag, + ChatComponents.icon, + ChatComponents.entityList, + ChatComponents.listBlock, + ChatComponents.listItem, + ChatComponents.text, + ChatComponents.boldText, + ChatComponents.iconText, + ChatComponents.imageText, + ChatComponents.imageTextLarge, + ChatComponents.metricIndicatorInline, + ChatComponents.metricIndicatorWithStrikethrough, + ChatComponents.snippetCardItem, + ChatComponents.snippetCardBlock, + ChatComponents.overviewCardItem, + ChatComponents.overviewCardBlock, + ChatComponents.contextCardItem, + ChatComponents.contextCardBlock, + ChatComponents.compositeCardItem, + ChatComponents.compositeCardBlock, + ChatComponents.visualCardItem, + ChatComponents.visualCardBlock, + modal, + ] + + /// Example responses for the prompt (`openuiExamples`). + public static let examples: [String] = [ + "Example 1 — Table (column-oriented):\n\nroot = Stack([title, tbl])\ntitle = TextContent(\"Top Languages\", \"large-heavy\")\ntbl = Table([Col(\"Language\", langs), Col(\"Users (M)\", users), Col(\"Year\", years)])\nlangs = [\"Python\", \"JavaScript\", \"Java\", \"TypeScript\", \"Go\"]\nusers = [15.7, 14.2, 12.1, 8.5, 5.2]\nyears = [1991, 1995, 1995, 2012, 2009]", + "Example 2 — Bar chart:\n\nroot = Stack([title, chart])\ntitle = TextContent(\"Q4 Revenue\", \"large-heavy\")\nchart = BarChart(labels, [s1, s2], \"grouped\")\nlabels = [\"Oct\", \"Nov\", \"Dec\"]\ns1 = Series(\"Product A\", [120, 150, 180])\ns2 = Series(\"Product B\", [90, 110, 140])", + "Example 3 — Form with validation:\n\nroot = Stack([title, form])\ntitle = TextContent(\"Contact Us\", \"large-heavy\")\nform = Form(\"contact\", btns, [nameField, emailField, countryField, msgField])\nnameField = FormControl(\"Name\", Input(\"name\", \"Your name\", \"text\", { required: true, minLength: 2 }))\nemailField = FormControl(\"Email\", Input(\"email\", \"you@example.com\", \"email\", { required: true, email: true }))\ncountryField = FormControl(\"Country\", Select(\"country\", countryOpts, \"Select...\", { required: true }))\nmsgField = FormControl(\"Message\", TextArea(\"message\", \"Tell us more...\", 4, { required: true, minLength: 10 }))\ncountryOpts = [SelectItem(\"us\", \"United States\"), SelectItem(\"uk\", \"United Kingdom\"), SelectItem(\"de\", \"Germany\")]\nbtns = Buttons([Button(\"Submit\", Action([@ToAssistant(\"Submit\")]), \"primary\"), Button(\"Cancel\", Action([@ToAssistant(\"Cancel\")]), \"secondary\")])", + "Example 4 — Tabs with mixed content:\n\nroot = Stack([title, tabs])\ntitle = TextContent(\"React vs Vue\", \"large-heavy\")\ntabs = Tabs([tabReact, tabVue])\ntabReact = TabItem(\"react\", \"React\", reactContent)\ntabVue = TabItem(\"vue\", \"Vue\", vueContent)\nreactContent = [TextContent(\"React is a library by Meta for building UIs.\"), Callout(\"info\", \"Note\", \"React uses JSX syntax.\")]\nvueContent = [TextContent(\"Vue is a progressive framework by Evan You.\"), Callout(\"success\", \"Tip\", \"Vue has a gentle learning curve.\")]", + "Example 5 — KPI and product cards:\n\nroot = Stack([kpiHeader, kpiBlock, productsHeader, productCards])\nkpiHeader = InlineHeader(\"This Month\", \"Key account metrics\")\nkpiBlock = SnippetCardBlock([kpi1, kpi2, kpi3])\nkpi1 = SnippetCardItem(\"revenue\", kpi1lhs, kpi1rhs)\nkpi1lhs = IconText(kpi1icon, \"neutral\", \"m\", \"Revenue\", \"Month to date\", false, \"horizontal\")\nkpi1icon = Icon(\"circle-dollar-sign\", \"finance\")\nkpi1rhs = BoldText(\"number\", \"$48,200\", \"+8.1%\", \"metric\")\nkpi2 = SnippetCardItem(\"active-users\", kpi2lhs, kpi2rhs)\nkpi2lhs = IconText(kpi2icon, \"neutral\", \"m\", \"Active Users\", \"This month\", false, \"horizontal\")\nkpi2icon = Icon(\"users\", \"people\")\nkpi2rhs = BoldText(\"number\", \"1,204\", \"+3.4%\", \"metric\")\nkpi3 = SnippetCardItem(\"churn\", kpi3lhs, kpi3rhs)\nkpi3lhs = IconText(kpi3icon, \"neutral\", \"m\", \"Churn\", \"This month\", false, \"horizontal\")\nkpi3icon = Icon(\"user-minus\", \"people\")\nkpi3rhs = BoldText(\"number\", \"1.8%\", \"-0.3%\", \"metric\")\nproductsHeader = InlineHeader(\"Top Products\", \"By units sold\")\nproductCards = CompositeCardBlock([p1, p2])\np1 = CompositeCardItem(\"widget-pro\", p1header, [p1body], p1footer)\np1header = IconText(p1icon, \"neutral\", \"m\", \"Widget Pro\", \"Best seller\", true, \"horizontal\")\np1icon = Icon(\"package\", \"shopping\")\np1body = MetricIndicatorInline(\"2,410 units\", \"Sold this month\", { direction: \"up\", value: 12 })\np1footer = { price: BoldText(\"text\", \"$29.00\"), button: p1btn }\np1btn = Button(\"View Details\", Action([@ToAssistant(\"Show details for Widget Pro\")]), \"secondary\")\np2 = CompositeCardItem(\"widget-mini\", p2header, [p2body], p2footer)\np2header = IconText(p2icon, \"neutral\", \"m\", \"Widget Mini\", \"New arrival\", true, \"horizontal\")\np2icon = Icon(\"box\", \"shopping\")\np2body = MetricIndicatorInline(\"980 units\", \"Sold this month\", { direction: \"up\", value: 4 })\np2footer = { price: BoldText(\"text\", \"$14.00\"), button: p2btn }\np2btn = Button(\"View Details\", Action([@ToAssistant(\"Show details for Widget Mini\")]), \"secondary\")", + "Example 6 — Editable table and a selection form:\n\nroot = Stack([tableHeader, editTable, formHeader, prefsForm])\ntableHeader = InlineHeader(\"Team Roster\", \"Click a cell to edit\")\neditTable = EditableTable(\"roster\", [colName, colRole, colStart], [row1, row2])\ncolName = { type: \"text\", key: \"name\", header: \"Name\" }\ncolRole = { type: \"select\", key: \"role\", header: \"Role\", options: [roleEng, roleDesign] }\nroleEng = { value: \"eng\", label: \"Engineering\" }\nroleDesign = { value: \"design\", label: \"Design\" }\ncolStart = { type: \"date-single\", key: \"start\", header: \"Start Date\" }\nrow1 = { id: \"1\", values: [\"Alex Kim\", \"eng\", \"2024-01-15\"] }\nrow2 = { id: \"2\", values: [\"Jamie Lee\", \"design\", \"2024-03-02\"] }\nformHeader = InlineHeader(\"Preferences\", \"Tell us how you like to work\")\nprefsForm = Form(\"prefs\", formButtons, [fc1, fc2])\nfc1 = FormControl(\"Working Style\", styleCards, \"Pick the one that fits best\")\nstyleCards = OptionCards(\"style\", \"single\", [style1, style2])\nstyle1 = OptionCard(\"focused\", \"Deep Focus\", \"Long uninterrupted blocks\", style1icon)\nstyle1icon = Icon(\"target\", \"tools\")\nstyle2 = OptionCard(\"collab\", \"Collaborative\", \"Frequent pairing and syncs\", style2icon)\nstyle2icon = Icon(\"users\", \"people\")\nfc2 = FormControl(\"Tools\", toolChips, \"Select all that apply\")\ntoolChips = Chips(\"tools\", \"multiple\", [tool1, tool2, tool3])\ntool1 = ChipItem(\"figma\", \"Figma\", tool1icon)\ntool1icon = Icon(\"figma\", \"design\")\ntool2 = ChipItem(\"slack\", \"Slack\", tool2icon)\ntool2icon = Icon(\"message-square\", \"communication\")\ntool3 = ChipItem(\"notion\", \"Notion\", tool3icon)\ntool3icon = Icon(\"notebook\", \"text\")\nformButtons = Buttons([Button(\"Save\", Action([@ToAssistant(\"Save preferences\")]), \"primary\")])", + ] + + /// Extra prompt rules for this library (`openuiAdditionalRules`). + public static let additionalRules: [String] = [ + "When asked about data, generate realistic/plausible data", + "For grid-like layouts, use Stack with direction \"row\" and wrap=true. Avoid justify=\"between\" unless you specifically want large gutters.", + "For forms, define one FormControl reference per field so controls can stream progressively.", + "For forms, always provide the second Form argument with Buttons(...) actions: Form(name, buttons, fields).", + "Never nest Form inside Form.", + "Use @Reset($var1, $var2) after form submit to restore defaults — not @Set($var, \"\")", + "Multi-query refresh: Action([@Run(mutation), @Run(query1), @Run(query2), @Reset(...)])", + "$variables are reactive: changing via Select or @Set re-evaluates all Queries and expressions referencing them", + "Use existing components (Tabs, Accordion, Modal) before inventing ternary show/hide patterns", + "Card blocks (SnippetCardBlock, OverviewCardBlock, ContextCardBlock, CompositeCardBlock, VisualCardBlock) need at least 2 items; every item in a block must have the same structure.", + "Text / BoldText / IconText / ImageText / ImageTextLarge / MetricIndicatorInline / MetricIndicatorWithStrikethrough are inline building blocks used INSIDE card items — do not place them directly in a Stack or Card.", + "EntityList size='small' (no header/footer) is only for use inside a CompositeCardItem body; use size='default' everywhere else.", + "Image URLs must be real (from a tool result or the user) — never invent or template an image URL.", + "Always pass a category to Icon — it enables a topical fallback when the exact icon name is unavailable.", + "EditableTable requires the consuming app to persist edits — use it only when the user or app explicitly asks for an editable table; default to Table for read-only data.", + ] + + /// The prompt options react-ui pairs with this library (`openuiPromptOptions`). + public static let promptOptions = PromptOptions( + additionalRules: additionalRules, examples: examples) +} diff --git a/packages/swift-lang/Sources/OpenUILang/Parser.swift b/packages/swift-lang/Sources/OpenUILang/Parser.swift new file mode 100644 index 0000000000..2a0f8d7955 --- /dev/null +++ b/packages/swift-lang/Sources/OpenUILang/Parser.swift @@ -0,0 +1,546 @@ +/// An insertion-ordered statement map with JavaScript `Map` semantics: +/// re-setting a key replaces the value but keeps its original position. +struct StatementMap { + private(set) var order: [String] = [] + private var storage: [String: Statement] = [:] + + var count: Int { order.count } + var values: [Statement] { order.map { storage[$0]! } } + + subscript(id: String) -> Statement? { storage[id] } + + func contains(_ id: String) -> Bool { storage[id] != nil } + + mutating func set(_ statement: Statement) { + if storage.updateValue(statement, forKey: statement.id) == nil { order.append(statement.id) } + } + + mutating func removeAll() { + order.removeAll() + storage.removeAll() + } +} + +// MARK: - Statement classification + +/// The `$variable` names referenced anywhere in a node, in first-seen order. +func collectQueryDeps(_ node: ASTNode?) -> [String] { + guard let node else { return [] } + var refs: [String] = [] + var seen: Set = [] + node.walk { current in + if case .stateRef(let name) = current, seen.insert(name).inserted { refs.append(name) } + } + return refs +} + +private func classifyStatement(_ raw: RawStatement, _ expr: ASTNode) -> Statement { + // Query is checked before $var so `$foo = Query(...)` is still a query. + if case .comp(let name, let args, _) = expr, name == ReservedCall.query { + let deps = collectQueryDeps(args.count > 1 ? args[1] : nil) + return .query( + id: raw.id, call: CallNode(callee: ReservedCall.query, args: args), expr: expr, + deps: deps.isEmpty ? nil : deps) + } + if case .comp(let name, let args, _) = expr, name == ReservedCall.mutation { + return .mutation( + id: raw.id, call: CallNode(callee: ReservedCall.mutation, args: args), expr: expr) + } + if raw.idTokenType == .stateVar { return .state(id: raw.id, initial: expr) } + return .value(id: raw.id, expr: expr) +} + +private func extractStatements(_ statements: [Statement], _ ctx: MaterializeContext) -> ( + OpenUIObject, [QueryStatementInfo], [MutationStatementInfo] +) { + var stateDeclarations = OpenUIObject() + var queries: [QueryStatementInfo] = [] + var mutations: [MutationStatementInfo] = [] + + for statement in statements { + switch statement { + case .state(let id, let initial): + stateDeclarations[id] = materializeValue(initial, ctx) + case .query(let id, let call, _, let deps): + let args = call.args + queries.append( + QueryStatementInfo( + statementId: id, toolAST: args.first, argsAST: args.count > 1 ? args[1] : nil, + defaultsAST: args.count > 2 ? args[2] : nil, refreshAST: args.count > 3 ? args[3] : nil, + deps: deps, complete: true)) + case .mutation(let id, let call, _): + let args = call.args + mutations.append( + MutationStatementInfo( + statementId: id, toolAST: args.first, argsAST: args.count > 1 ? args[1] : nil)) + case .value: + break + } + } + + // Any $var referenced but never declared defaults to null. + for statement in statements { + let nodes: [ASTNode] + switch statement { + case .state(_, let initial): nodes = [initial] + case .value(_, let expr): nodes = [expr] + case .query(_, let call, _, _), .mutation(_, let call, _): nodes = call.args + } + for node in nodes { + for dep in collectQueryDeps(node) where !stateDeclarations.contains(dep) { + stateDeclarations[dep] = .null + } + } + } + + return (stateDeclarations, queries, mutations) +} + +private let defaultRootStatementId = "root" + +private func isComponentStatement(_ statement: Statement) -> String? { + guard case .value(_, .comp(let name, _, _)) = statement, !isBuiltin(name), + name != ReservedCall.query, name != ReservedCall.mutation + else { return nil } + return name +} + +/// The entry statement: `root`, then a statement named after the library root, +/// then the first statement calling the root component, then the first +/// component statement, then the first statement. +private func pickEntryId( + _ map: StatementMap, _ statements: [Statement], _ firstId: String, _ rootName: String? +) -> String { + if map.contains(defaultRootStatementId) { return defaultRootStatementId } + if let rootName, map.contains(rootName) { return rootName } + if let rootName, + let preferred = statements.first(where: { isComponentStatement($0) == rootName }) + { + return preferred.id + } + return statements.first(where: { isComponentStatement($0) != nil })?.id ?? firstId +} + +private func buildResult( + _ map: StatementMap, _ statements: [Statement], _ firstId: String, _ wasIncomplete: Bool, + _ statementCount: Int, _ cat: ParamMap?, _ rootName: String? +) -> ParseResult { + let entryId = pickEntryId(map, statements, firstId, rootName) + guard map.contains(entryId) else { return .empty(incomplete: wasIncomplete) } + + var syms: [String: ASTNode] = [:] + var unreached: [String] = [] + for id in map.order { + guard let statement = map[id] else { continue } + switch statement { + case .state(_, let initial): + syms[id] = initial + case .value(_, let expr), .query(_, _, let expr, _), .mutation(_, _, let expr): + syms[id] = expr + } + // Orphan tracking covers value statements other than the entry; state, + // Query and Mutation declarations are consumed separately. + if id != entryId, case .value = statement { unreached.append(id) } + } + + let ctx = MaterializeContext( + syms: syms, cat: cat, partial: wasIncomplete, currentStatementId: entryId) + ctx.unreached = unreached + var root: ElementNode? = nil + if let entry = syms[entryId], case .element(var element) = materializeValue(entry, ctx) { + element.statementId = entryId + root = element + } + + let (stateDeclarations, queries, mutations) = extractStatements(statements, ctx) + + return ParseResult( + root: root, + meta: .init( + incomplete: wasIncomplete, unresolved: ctx.unres, orphaned: ctx.unreached ?? [], + statementCount: statementCount, errors: ctx.errors), + stateDeclarations: stateDeclarations, queryStatements: queries, mutationStatements: mutations) +} + +// MARK: - Preprocessing + +private let quote = UInt16(ascii: "\"") +private let singleQuote = UInt16(ascii: "'") +private let backslash = UInt16(ascii: "\\") +private let backtick = UInt16(ascii: "`") +private let newline = UInt16(ascii: "\n") + +/// Index after a double-quoted string starting at `start`, or `start` if none starts there. +private func skipString(_ input: [UInt16], _ start: Int) -> Int { + guard start < input.count, input[start] == quote else { return start } + var i = start + 1 + while i < input.count { + if input[i] == backslash { + i += 2 + } else if input[i] == quote { + return i + 1 + } else { + i += 1 + } + } + return i +} + +private func isFence(_ input: [UInt16], _ i: Int) -> Bool { + i + 2 < input.count && input[i] == backtick && input[i + 1] == backtick + && input[i + 2] == backtick +} + +private func string(_ units: ArraySlice) -> String { + String(decoding: units, as: UTF16.self) +} + +/// Extracts code from markdown fences (joining multiple blocks), or returns the +/// input unchanged when there are none. Fences inside double-quoted strings are ignored. +public func stripFences(_ text: String) -> String { + let input = Array(text.utf16) + var blocks: [String] = [] + var i = 0 + + while i < input.count { + var fenceStart = -1 + while i < input.count { + let next = skipString(input, i) + if next > i { + i = next + continue + } + if isFence(input, i) { + fenceStart = i + break + } + i += 1 + } + if fenceStart == -1 { break } + + // Skip the language tag. + var j = fenceStart + 3 + while j < input.count && input[j] != newline { j += 1 } + if j >= input.count { + // No newline after the opening fence yet (streaming): the rest is the tag line. + blocks.append("") + i = input.count + break + } + j += 1 + + var closePos = -1 + var k = j + while k < input.count { + let next = skipString(input, k) + if next > k { + k = next + continue + } + if isFence(input, k) { + closePos = k + break + } + k += 1 + } + + if closePos != -1 { + blocks.append(string(input[j.. Int? { + guard units.count >= 3 else { return nil } + for i in stride(from: units.count - 3, through: 0, by: -1) where isFence(units, i) { return i } + return nil +} + +/// Strips `//` and `#` line comments outside strings. String state carries +/// across lines, as in lang-core. +private func stripComments(_ text: String) -> String { + var inString: UInt16? = nil + // Split on UTF-16 newlines like `split("\n")`; Swift treats "\r\n" as one Character. + let lines = Array(text.utf16).split(separator: newline, omittingEmptySubsequences: false) + return lines.map { line -> String in + let units = Array(line) + var i = 0 + while i < units.count { + let c = units[i] + if let quoteChar = inString { + if c == backslash && i + 1 < units.count { + i += 2 + continue + } + if c == quoteChar { inString = nil } + i += 1 + continue + } + if c == quote || c == singleQuote { + inString = c + i += 1 + continue + } + if c == UInt16(ascii: "/") && i + 1 < units.count && units[i + 1] == UInt16(ascii: "/") { + return jsTrimEnd(string(units[.. String { + jsTrim(stripComments(stripFences(jsTrim(input)))) +} + +// MARK: - Parsing + +/// Compiles positional parameter lists from a library JSON Schema +/// (`$defs` keyed by component name, properties in positional order). +public func compileSchema(_ schema: OpenUIValue) -> ParamMap { + var map = ParamMap() + guard let defs = schema["$defs"].objectValue else { return map } + let components = schema["properties"].objectValue.map { Set($0.keys) } + for (name, def) in defs { + // Skip non-component defs, e.g. zod's hoisted recursive schemas (__schema0). + if let components, !components.contains(name) { continue } + let properties = def["properties"].objectValue ?? OpenUIObject() + let required = (def["required"].arrayValue ?? []).compactMap(\.stringValue) + map[name] = properties.keys.map { key in + ParamDef( + name: key, required: required.contains(key), + defaultValue: schemaDefaultValue(properties[key]), schema: properties[key]) + } + } + return map +} + +/// Parses a complete (or partial) OpenUI Lang response in one pass. +public func parse(_ input: String, _ cat: ParamMap?, rootName: String? = nil) -> ParseResult { + let trimmed = preprocess(input) + if trimmed.isEmpty { return .empty() } + + let (text, wasIncomplete) = autoClose(trimmed) + let rawStatements = splitStatements(tokenize(text)) + if rawStatements.isEmpty { return .empty(incomplete: wasIncomplete) } + + var map = StatementMap() + var firstId = "" + for raw in rawStatements { + map.set(classifyStatement(raw, parseExpression(raw.tokens))) + if firstId.isEmpty { firstId = raw.id } + } + return buildResult(map, map.values, firstId, wasIncomplete, map.count, cat, rootName) +} + +/// A parser that keeps completed statements between calls, so each streamed +/// chunk only re-parses the statement still being written. +public final class StreamParser { + private let cat: ParamMap? + private let rootName: String? + + /// Raw accumulated input, kept for `set(_:)` diffing. + private var buffer: [UInt16] = [] + /// Preprocessed view of the buffer. Statement boundaries are scanned on this, + /// so markdown prose or fences before the program can't confuse them. + private var cleaned: [UInt16] = [] + /// How far into `cleaned` statements are complete. + private var completedEnd = 0 + private var completedMap = StatementMap() + private var completedCount = 0 + private var firstId = "" + + public init(_ cat: ParamMap?, rootName: String? = nil) { + self.cat = cat + self.rootName = rootName + } + + /// Feeds the next chunk and returns the latest result. + public func push(_ chunk: String) -> ParseResult { + buffer.append(contentsOf: chunk.utf16) + return currentResult() + } + + /// Sets the full text so far. Appended text is diffed against the buffer; + /// replaced text resets the parser. + public func set(_ fullText: String) -> ParseResult { + let units = Array(fullText.utf16) + if units.count < buffer.count || !units.starts(with: buffer) { reset() } + if units.count > buffer.count { buffer.append(contentsOf: units[buffer.count...]) } + return currentResult() + } + + /// The latest result without consuming new data. + public func result() -> ParseResult { currentResult() } + + private func reset() { + buffer = [] + cleaned = [] + completedEnd = 0 + completedMap.removeAll() + completedCount = 0 + firstId = "" + } + + private func addStatements(_ text: String) { + let trimmed = jsTrim(text) + if trimmed.isEmpty { return } + for raw in splitStatements(tokenize(trimmed)) { + completedMap.set(classifyStatement(raw, parseExpression(raw.tokens))) + completedCount += 1 + if firstId.isEmpty { firstId = raw.id } + } + } + + /// Recomputes `cleaned`. If the completed prefix moved (e.g. an opening fence + /// just appeared), the cache is stale and scanning restarts. + private func refreshCleaned() { + let next = Array(preprocess(String(decoding: buffer, as: UTF16.self)).utf16) + if !next.starts(with: cleaned.prefix(completedEnd)) { + completedEnd = 0 + completedMap.removeAll() + completedCount = 0 + firstId = "" + } + cleaned = next + } + + /// Moves completed statements into the cache; returns where the pending one starts. + private func scanNewCompleted() -> Int { + var depth = 0 + var ternaryDepth = 0 + var inString: UInt16? = nil + var escaped = false + var statementStart = completedEnd + + var i = completedEnd + while i < cleaned.count { + defer { i += 1 } + let c = cleaned[i] + if escaped { + escaped = false + continue + } + if c == backslash && inString != nil { + escaped = true + continue + } + if let quoteChar = inString { + if c == quoteChar { inString = nil } + continue + } + if c == quote || c == singleQuote { + inString = c + continue + } + switch c { + case UInt16(ascii: "("), UInt16(ascii: "["), UInt16(ascii: "{"): + depth += 1 + case UInt16(ascii: ")"), UInt16(ascii: "]"), UInt16(ascii: "}"): + depth = max(0, depth - 1) + case UInt16(ascii: "?") where depth == 0: + ternaryDepth += 1 + case UInt16(ascii: ":") where depth == 0 && ternaryDepth > 0: + ternaryDepth -= 1 + case newline where depth <= 0 && ternaryDepth <= 0: + // A following `?` (or `:` inside a ternary) continues the statement. + var peek = i + 1 + while peek < cleaned.count, + [UInt16(ascii: " "), UInt16(ascii: "\t"), UInt16(ascii: "\r"), newline].contains( + cleaned[peek]) + { + peek += 1 + } + if peek < cleaned.count, + cleaned[peek] == UInt16(ascii: "?") + || (cleaned[peek] == UInt16(ascii: ":") && ternaryDepth > 0) + { + continue + } + let text = jsTrim(string(cleaned[statementStart.. ParseResult { + refreshCleaned() + let pendingStart = scanNewCompleted() + let pendingText = jsTrim(string(cleaned[min(pendingStart, cleaned.count)...])) + + if pendingText.isEmpty { + if completedCount == 0 { return .empty() } + return buildResult( + completedMap, completedMap.values, firstId, false, completedCount, cat, rootName) + } + + // `cleaned` is already preprocessed; only the trailing statement needs closing. + let (closed, wasIncomplete) = autoClose(pendingText) + let rawStatements = splitStatements(tokenize(closed)) + + if rawStatements.isEmpty { + if completedCount == 0 { return .empty(incomplete: wasIncomplete) } + return buildResult( + completedMap, completedMap.values, firstId, wasIncomplete, completedCount, cat, rootName) + } + + // New ids render progressively; an existing id is only replaced once its + // pending expression no longer needs closing. + var allMap = completedMap + for raw in rawStatements { + if completedMap.contains(raw.id) && wasIncomplete { continue } + allMap.set(classifyStatement(raw, parseExpression(raw.tokens))) + } + let entryFallback = firstId.isEmpty ? rawStatements[0].id : firstId + return buildResult( + allMap, allMap.values, entryFallback, wasIncomplete, completedCount + rawStatements.count, + cat, rootName) + } +} + +/// A batch parser bound to a library's JSON Schema. +public struct Parser: Sendable { + public let paramMap: ParamMap + public let rootName: String? + + /// - Parameter schema: the library JSON Schema (`library.toJSONSchema()` in lang-core). + public init(schema: OpenUIValue, rootName: String? = nil) { + self.paramMap = compileSchema(schema) + self.rootName = rootName + } + + public func parse(_ input: String) -> ParseResult { + OpenUILang.parse(input, paramMap, rootName: rootName) + } + + public func makeStreamParser() -> StreamParser { + StreamParser(paramMap, rootName: rootName) + } +} diff --git a/packages/swift-lang/Sources/OpenUILang/Prompt.swift b/packages/swift-lang/Sources/OpenUILang/Prompt.swift new file mode 100644 index 0000000000..6dca0c76b0 --- /dev/null +++ b/packages/swift-lang/Sources/OpenUILang/Prompt.swift @@ -0,0 +1,676 @@ +/// A tool the model can call through `Query()` / `Mutation()`. Shaped like an +/// MCP tool (name, description, input and output JSON Schemas). +public struct ToolSpec: Sendable, Equatable { + public var name: String + public var description: String? + public var inputSchema: OpenUIValue + public var outputSchema: OpenUIValue + + public init( + name: String, description: String? = nil, inputSchema: OpenUIValue = [:], + outputSchema: OpenUIValue = .undefined + ) { + self.name = name + self.description = description + self.inputSchema = inputSchema + self.outputSchema = outputSchema + } +} + +/// A tool listed in the prompt: a bare name or a full spec. +public enum ToolDescriptor: Sendable, Equatable { + case name(String) + case spec(ToolSpec) +} + +/// A component's prompt entry: its signature and description. +public struct ComponentPromptSpec: Sendable, Equatable { + public var signature: String + public var description: String? + + public init(signature: String, description: String? = nil) { + self.signature = signature + self.description = description + } +} + +/// A named group of components in the prompt, with optional notes. +public struct ComponentGroup: Sendable, Equatable { + public var name: String + public var components: [String] + public var notes: [String]? + + public init(name: String, components: [String], notes: [String]? = nil) { + self.name = name + self.components = components + self.notes = notes + } +} + +/// Options that shape the generated system prompt. +public struct PromptOptions: Sendable, Equatable { + public var preamble: String? + public var additionalRules: [String]? + public var examples: [String]? + public var toolExamples: [String]? + public var tools: [ToolDescriptor]? + public var editMode: Bool? + public var inlineMode: Bool? + /// Enables Query(), Mutation(), @Run. Defaults to true when tools are given. + public var toolCalls: Bool? + /// Enables $variables, @Set, @Reset. Defaults to `toolCalls`. + public var bindings: Bool? + + public init( + preamble: String? = nil, additionalRules: [String]? = nil, examples: [String]? = nil, + toolExamples: [String]? = nil, tools: [ToolDescriptor]? = nil, editMode: Bool? = nil, + inlineMode: Bool? = nil, toolCalls: Bool? = nil, bindings: Bool? = nil + ) { + self.preamble = preamble + self.additionalRules = additionalRules + self.examples = examples + self.toolExamples = toolExamples + self.tools = tools + self.editMode = editMode + self.inlineMode = inlineMode + self.toolCalls = toolCalls + self.bindings = bindings + } +} + +/// Everything prompt generation needs: the library's component signatures and the options. +public struct PromptSpec: Sendable, Equatable { + public var root: String? + /// Component entries in library order. + public var components: [(name: String, spec: ComponentPromptSpec)] + public var componentGroups: [ComponentGroup]? + public var options: PromptOptions + + public init( + root: String?, components: [(name: String, spec: ComponentPromptSpec)], + componentGroups: [ComponentGroup]? = nil, options: PromptOptions = PromptOptions() + ) { + self.root = root + self.components = components + self.componentGroups = componentGroups + self.options = options + } + + public static func == (lhs: PromptSpec, rhs: PromptSpec) -> Bool { + lhs.root == rhs.root && lhs.components.map(\.name) == rhs.components.map(\.name) + && lhs.components.map(\.spec) == rhs.components.map(\.spec) + && lhs.componentGroups == rhs.componentGroups && lhs.options == rhs.options + } +} + +// MARK: - JSON Schema helpers + +private func jsonSchemaTypeString(_ schema: OpenUIValue) -> String { + switch schema["type"].stringValue { + case "string": + if let values = schema["enum"].arrayValue { + return values.map { "\"\($0.jsString)\"" }.joined(separator: " | ") + } + return "string" + case "number", "integer": + return "number" + case "boolean": + return "boolean" + case "array": + if case .object = schema["items"] { return "\(jsonSchemaTypeString(schema["items"]))[]" } + return "any[]" + case "object": + if let props = schema["properties"].objectValue, !props.isEmpty { + let required = (schema["required"].arrayValue ?? []).compactMap(\.stringValue) + let fields = props.entries.map { key, value in + "\(key)\(required.contains(key) ? "" : "?"): \(jsonSchemaTypeString(value))" + } + return "{\(fields.joined(separator: ", "))}" + } + return "object" + default: + return "any" + } +} + +/// A placeholder value shaped like an output schema, for Query defaults hints. +private func defaultForSchema(_ schema: OpenUIValue) -> OpenUIValue { + switch schema["type"].stringValue { + case "string": return "" + case "number", "integer": return 0 + case "boolean": return false + case "array": return [] + case "object": + guard let props = schema["properties"].objectValue, !props.isEmpty else { return [:] } + return .object(OpenUIObject(props.entries.map { ($0.key, defaultForSchema($0.value)) })) + default: return .null + } +} + +// MARK: - Sections + +private let defaultPreamble = + "You are an AI assistant that responds using openui-lang, a declarative UI language. Your ENTIRE response must be valid openui-lang code — no markdown, no explanations, just openui-lang." + +private func syntaxRules(_ rootName: String, supportsExpressions: Bool, bindings: Bool) -> String { + var lines = [ + "## Syntax Rules", + "", + "1. Each statement is on its own line: `identifier = Expression`", + "2. `root` is the entry point — every program must define `root = \(rootName)(...)`", + "3. Expressions are: strings (\"...\"), numbers, booleans (true/false), null, arrays ([...]), objects ({...}), or component calls TypeName(arg1, arg2, ...)", + "4. Use references for readability: define `name = ...` on one line, then use `name` later", + "5. EVERY variable (except root) MUST be referenced by at least one other variable. Unreferenced variables are silently dropped and will NOT render. Always include defined variables in their parent's children/items array.", + "6. Arguments are POSITIONAL (order matters, not names). Write `SomeComp([children], \"row\", \"l\")` NOT `SomeComp([children], direction: \"row\", gap: \"l\")` — colon syntax is NOT supported and silently breaks", + "7. Optional arguments can be omitted from the end", + ] + var ruleNumber = 8 + func rule(_ text: String) { + lines.append("\(ruleNumber). \(text)") + ruleNumber += 1 + } + if bindings { + rule( + "Declare mutable state with `$varName = defaultValue`. Components marked with `$binding` can read/write these. Undeclared $variables are auto-created with null default." + ) + } + if supportsExpressions { + rule("String concatenation: `\"text\" + $var + \"more\"`") + rule( + "Dot member access: `query.field` reads a field; on arrays it extracts that field from every element" + ) + rule("Index access: `arr[0]`, `data[index]`") + rule( + "Arithmetic operators: +, -, *, /, % (work on numbers; + is string concat when either side is a string)" + ) + rule("Comparison: ==, !=, >, <, >=, <=") + rule("Logical: &&, ||, ! (prefix)") + rule("Ternary: `condition ? valueIfTrue : valueIfFalse`") + rule("Parentheses for grouping: `(a + b) * c`") + } + lines.append("- Strings use double quotes with backslash escaping") + return lines.joined(separator: "\n") +} + +private func builtinFunctionsSection() -> String { + let builtinLines = builtinOrder.compactMap { builtins[$0] }.map { + "@\($0.signature) — \($0.description)" + } + let lazyLines = lazyBuiltinDefs.keys.sorted().map { + "@\(lazyBuiltinDefs[$0]!.signature) — \(lazyBuiltinDefs[$0]!.description)" + } + let lines = (builtinLines + lazyLines).joined(separator: "\n") + return """ + ## Built-in Functions + + Data functions prefixed with `@` to distinguish from components. These are the ONLY functions available — do NOT invent new ones. + Use @-prefixed built-in functions (@Count, @Sum, @Avg, @Min, @Max, @Round) on Query results — do NOT hardcode computed values. + + \(lines) + + Builtins compose — output of one is input to the next: + `@Count(@Filter(data.rows, "field", "==", "val"))` for KPIs/chart values, `@Round(@Avg(data.rows.score), 1)`, `@Each(data.rows, "item", Comp(item.field))` for per-item rendering. + Array pluck: `data.rows.field` extracts a field from every row → use with @Sum, @Avg, charts, tables. + + IMPORTANT @Each rule: The loop variable (e.g. "item") is ONLY available inside the @Each template expression. Always inline the template — do NOT extract it to a separate statement. + CORRECT: `Col("Actions", @Each(rows, "t", Button("Edit", Action([@Set($id, t.id)]))))` + WRONG: `myBtn = Button("Edit", Action([@Set($id, t.id)]))` then `Col("Actions", @Each(rows, "t", myBtn))` — t is undefined in myBtn. + """ +} + +private let querySection = """ + ## Query — Live Data Fetching + + Fetch data from available tools. Returns defaults instantly, swaps in real data when it arrives. + + ``` + metrics = Query("tool_name", {arg1: value, arg2: $binding}, {defaultField: 0, defaultData: []}, refreshInterval?) + ``` + + - First arg: tool name (string) + - Second arg: arguments object (may reference $bindings — re-fetches automatically on change) + - Third arg: default data (rendered immediately before fetch resolves) + - Fourth arg (optional): refresh interval in seconds (e.g. 30 for auto-refresh every 30s) + - Use dot access on results: metrics.totalEvents, metrics.data.day (array pluck) + - Query results must use regular identifiers: `metrics = Query(...)`, NOT `$metrics = Query(...)` + - Manual refresh: `Button("Refresh", Action([@Run(query1), @Run(query2)]), "secondary")` — re-fetches the listed queries + - Refresh all queries: create Action with @Run for each query + """ + +private let mutationSection = """ + ## Mutation — Write Operations + + Execute state-changing tool calls (create, update, delete). Unlike Query (auto-fetches on render), Mutation fires only on button click via Action. + + ``` + result = Mutation("tool_name", {arg1: $binding, arg2: "value"}) + ``` + + - First arg: tool name (string) + - Second arg: arguments object (evaluated with current $binding values at click time) + - result.status: "idle" | "loading" | "success" | "error" + - result.data: tool response on success + - result.error: error message on failure + - Mutation results use regular identifiers: `result = Mutation(...)`, NOT `$result` + - Show loading state: `result.status == "loading" ? TextContent("Saving...") : null` + """ + +private func actionSection(toolCalls: Bool, bindings: Bool) -> String { + var steps = [ + "- @ToAssistant(\"message\") — Send a message to the assistant (for conversational buttons like \"Tell me more\", \"Explain this\")", + "- @OpenUrl(\"https://...\") — Navigate to a URL", + ] + if bindings { + steps.append("- @Set($variable, value) — Set a $variable to a specific value") + steps.append( + "- @Reset($var1, $var2, ...) — Reset $variables to their declared defaults (e.g. @Reset($title, $priority) restores $title=\"\" and $priority=\"medium\")" + ) + } + if toolCalls { + steps.insert( + "- @Run(queryOrMutationRef) — Execute a Mutation or re-fetch a Query (ref must be a declared Query/Mutation)", + at: 0) + } + + var examples: [String] = [] + if toolCalls { + examples.append( + """ + Example — mutation + refresh + reset (PREFERRED pattern): + ``` + $binding = "default" + result = Mutation("tool_name", {field: $binding}) + data = Query("tool_name", {}, {rows: []}) + onSubmit = Action([@Run(result), @Run(data), @Reset($binding)]) + ``` + """) + } + examples.append( + """ + Example — simple nav: + ``` + viewBtn = Button("View", Action([@OpenUrl("https://example.com")])) + ``` + """) + + var rules = [ + "- Action can be assigned to a variable or inlined: Button(\"Go\", onSubmit) and Button(\"Go\", Action([...])) both work" + ] + if toolCalls { + rules.append("- If a @Run(mutation) step fails, remaining steps are skipped (halt on failure)") + rules.append("- @Run(queryRef) re-fetches the query (fire-and-forget, cannot fail)") + } + + return """ + ## Action — Button Behavior + + Action([@steps...]) wires button clicks to operations. Steps are @-prefixed built-in actions. Steps execute in order. + Buttons without an explicit Action prop automatically send their label to the assistant (equivalent to Action([@ToAssistant(label)])). + + Available steps: + \(steps.joined(separator: "\n")) + + \(examples.joined(separator: "\n\n")) + + \(rules.joined(separator: "\n")) + """ +} + +private let interactiveFiltersSection = """ + ## Interactive Filters + + To let the user filter data with a dropdown: + 1. Declare a $variable with a default: `$dateRange = "14"` + 2. Create a Select with name, items, and binding: `Select("dateRange", [SelectItem("7", "Last 7 days"), ...], null, null, $dateRange)` + 3. Wrap in FormControl for a label: `FormControl("Date Range", Select(...))` + 4. Pass $dateRange in Query args: `Query("tool", {dateRange: $dateRange}, {defaults})` + 5. When the user changes the Select, $dateRange updates and the Query automatically re-fetches + + FILTER WIRING RULE: If a $binding filter is visible in the UI, EVERY relevant Query MUST reference that $binding in its args. Never show a filter dropdown while hardcoding the query args. + + Rules for $variables: + - $variables hold simple values (strings or numbers), NOT arrays or objects + - $variables must be bound to a Select/Input component via the value argument (last positional arg) to be interactive + - Queries must use regular identifiers (NOT $variables): `metrics = Query(...)` not `$metrics = Query(...)` + - **Auto-declare**: You do NOT need to explicitly declare $variables. If you use `$foo` without declaring it, the parser auto-creates `$foo = null`. You can still declare explicitly to set a default: `$days = "14"` + + ## Forms + + Simple form — no $bindings needed. Field values are managed internally by the Form via the name prop: + ``` + contactForm = Form("contact", submitBtn, [nameField, emailField]) + nameField = FormControl("Name", Input("name", "Your name", "text", {required: true})) + emailField = FormControl("Email", Input("email", "your@email.com", "email", {required: true, email: true})) + submitBtn = Button("Submit") + ``` + + Use $bindings when you need to read field values elsewhere (in Action context, Query args, or conditionals). They are auto-declared: + ``` + $role = "engineer" + contactForm = Form("contact", submitBtn, [nameField, emailField, roleField]) + nameField = FormControl("Name", Input("name", "Enter your name", "text", {required: true}, $name)) + emailField = FormControl("Email", Input("email", "Enter your email", "email", {required: true, email: true}, $email)) + roleField = FormControl("Role", Select("role", [SelectItem("engineer", "Engineer"), SelectItem("designer", "Designer"), SelectItem("pm", "PM")], null, {required: true}, $role)) + submitBtn = Button("Submit") + ``` + + For form + mutation patterns (create, refresh, reset), see the Action section example above. + + IMPORTANT: Always add validation rules to form fields used with Mutations. Use OBJECT syntax: {required: true, email: true, minLength: 8}. The renderer shows error messages automatically and blocks submit when validation fails. + """ + +private let editModeSection = """ + ## Edit Mode + + The runtime merges by statement name: same name = replace, new name = append. + Output ONLY statements that changed or are new. Everything else is kept automatically. + + ### Delete + To remove a component, re-declare its parent without that component in the parent's children array. The removed component and any statements only it referenced are automatically garbage-collected. + + ### Patch size guide + - Changing a title or label: 1 statement + - Adding a component: 2-3 statements (the new component + parent update) + - Removing a component: 1 statement (re-declare parent without the removed child) + - Adding a filter + wiring to query: 3-5 statements + - Restructuring into tabs: 5-10 statements + + ### Rules + - Reuse existing statement names exactly — do not rename + - Do NOT re-emit unchanged statements — the runtime keeps them + - A typical edit patch is 1-10 statements, not 20+ + - If the existing code already satisfies the request, output only the root statement + - NEVER output the entire program as a patch. Only output what actually changes + - If you are about to output more than 10 statements, reconsider — most edits need fewer + """ + +private func streamingRules(_ rootName: String, supportsExpressions: Bool) -> String { + var steps = ["1. `root = \(rootName)(...)` — UI shell appears immediately"] + if supportsExpressions { + steps.append("2. $variable declarations — state ready for bindings") + steps.append( + "3. Query statements — defaults resolve immediately so components render with data") + steps.append("4. Component definitions — fill in with data already available") + steps.append("5. Data values — leaf content last") + } else { + steps.append("2. Component definitions — fill in as they stream") + steps.append("3. Data values — leaf content last") + } + return """ + ## Hoisting & Streaming (CRITICAL) + + openui-lang supports hoisting: a reference can be used BEFORE it is defined. The parser resolves all references after the full input is parsed. + + During streaming, the output is re-parsed on every chunk. Undefined references are temporarily unresolved and appear once their definitions stream in. This creates a progressive top-down reveal — structure first, then data fills in. + + **Recommended statement order for optimal streaming:** + \(steps.joined(separator: "\n")) + + Always write the root = \(rootName)(...) statement first so the UI shell appears immediately, even before child data has streamed in. + """ +} + +private let inlineModeSection = """ + ## Inline Mode + + You are in inline mode. You can respond in two ways: + + ### 1. Code response (when the user wants to CREATE or CHANGE the UI) + Wrap openui-lang code in triple-backtick fences. You can include explanatory text before/after: + + Here's your dashboard: + + ```openui-lang + root = RootComp([header, content]) + header = SomeHeader("Title") + content = SomeContent("Hello world") + ``` + + I created a simple layout with a header. + + ### 2. Text-only response (when the user asks a QUESTION) + If the user asks "what is this?", "explain the chart", "how does this work", etc. — respond with plain text. Do NOT output any openui-lang code. The existing dashboard stays unchanged. + + ### Rules + - When the user asks for changes, output ONLY the changed/new statements in a fenced block + - When the user asks a question, respond with text only — NO code. The dashboard stays unchanged. + - The parser extracts code from fences automatically. Text outside fences is shown as chat. + """ + +private let toolWorkflowSection = """ + ## Data Workflow + + When tools are available, follow this workflow: + 1. FIRST: Call the most relevant tool to inspect the real data shape before generating code + 2. Use Query() for READ operations (data that should stay live) — NEVER hardcode tool results as literal arrays or objects + 3. Use Mutation() for WRITE operations (create, update, delete) — triggered by button clicks via Action([@Run(mutationRef)]) + 4. Use the real data from step 1 as condensed Query defaults (3-5 rows) so the UI renders immediately + 5. Use @-prefixed builtins (@Count, @Filter, @Sort, @Sum) on Query results for KPIs and aggregations — the runtime evaluates these live on every refresh + 6. Hardcoded arrays are ONLY for static display data (labels, options) where no tool exists + + WRONG — you called a tool and got data back, but you inlined the results: + ``` + openCount = 2 + item1 = SomeComp("first item title") + item2 = SomeComp("second item title") + list = SomeList([item1, item2]) + chart = SomeChart(["A", "B"], [12, 8]) + ``` + This is static — it shows stale data and won't update. Creating item1, item2, item3... manually is ALWAYS wrong when a tool exists. + + RIGHT — use Query() for live data, Mutation() for writes, @builtins to derive values: + ``` + data = Query("tool_name", {}, {rows: []}) + openCount = @Count(@Filter(data.rows, "field", "==", "value")) + list = @Each(data.rows, "item", SomeComp(item.title, item.field)) + createResult = Mutation("create_tool", {title: $title}) + submitBtn = Button("Create", Action([@Run(createResult), @Run(data), @Reset($title)])) + ``` + Everything derives from the Query — when data refreshes, the entire dashboard updates automatically. + """ + +private func importantRules(_ rootName: String, toolCalls: Bool, bindings: Bool) -> String { + var verify = [ + "1. root = \(rootName)(...) is the FIRST line (for optimal streaming).", + "2. Every referenced name is defined. Every defined name (other than root) is reachable from root.", + ] + if toolCalls { verify.append("3. Every Query result is referenced by at least one component.") } + if bindings { + verify.append( + "\(toolCalls ? "4" : "3"). Every $binding appears in at least one component or expression.") + } + if toolCalls && bindings { + verify.append("5. Every visible filter $binding appears in at least one Query args object.") + } + return """ + ## Important Rules + - Choose components that best represent the content (tables for comparisons, charts for trends, forms for input, etc.) + + ## Final Verification + Before finishing, walk your output and verify: + \(verify.joined(separator: "\n")) + """ +} + +// MARK: - Tools + +private func renderToolSignature(_ tool: ToolSpec) -> String { + var args = "" + if let props = tool.inputSchema["properties"].objectValue, !props.isEmpty { + let required = (tool.inputSchema["required"].arrayValue ?? []).compactMap(\.stringValue) + args = props.entries.map { key, value in + "\(key)\(required.contains(key) ? "" : "?"): \(jsonSchemaTypeString(value))" + }.joined(separator: ", ") + } + let returnType = tool.outputSchema.isTruthy ? " → \(jsonSchemaTypeString(tool.outputSchema))" : "" + var line = "- \(tool.name)(\(args))\(returnType)" + if let description = tool.description, !description.isEmpty { line += "\n \(description)" } + return line +} + +private func renderToolsSection(_ tools: [ToolDescriptor]) -> String { + var lines = [ + "## Available Tools", + "", + "Use these with Query() for read operations or Mutation() for write operations. The LLM decides which is appropriate based on the tool's purpose.", + "", + ] + var specTools: [ToolSpec] = [] + for tool in tools { + if case .name(let name) = tool { lines.append("- \(name)") } + if case .spec(let spec) = tool { specTools.append(spec) } + } + for tool in specTools { lines.append(renderToolSignature(tool)) } + + let withOutput = specTools.filter { $0.outputSchema.isTruthy } + if !withOutput.isEmpty { + lines.append("") + lines.append("### Default values for Query results") + lines.append("") + lines.append("Use these shapes as minimal Query defaults:") + for tool in withOutput { + lines.append("- \(tool.name): `\(JSON.stringify(defaultForSchema(tool.outputSchema)))`") + } + } + lines.append("") + lines.append( + "CRITICAL: Use ONLY the tools listed above in Query() and Mutation() calls. Do NOT invent or guess tool names. If the user asks for functionality that doesn't match any available tool, use realistic mock data instead of fabricating a tool call." + ) + return lines.joined(separator: "\n") +} + +// MARK: - Component signatures + +private func componentSignatures( + _ spec: PromptSpec, toolCalls: Bool, bindings: Bool, usesActionExpression: Bool +) -> String { + var lines = [ + "## Component Signatures", + "", + "Arguments marked with ? are optional. Sub-components can be inline or referenced; prefer references for better streaming.", + ] + if usesActionExpression { + let steps = [ + toolCalls ? "@Run" : "", "@ToAssistant", "@OpenUrl", bindings ? "@Set" : "", + bindings ? "@Reset" : "", + ].filter { !$0.isEmpty } + lines.append( + "Props typed `ActionExpression` accept an Action([@steps...]) expression. See the Action section for available steps (\(steps.joined(separator: ", ")))." + ) + } + if bindings || spec.components.contains(where: { $0.spec.signature.contains("$binding") }) { + lines.append( + "Props marked `$binding` accept a `$variable` reference for two-way binding.") + } + + func format(_ component: ComponentPromptSpec) -> String { + if let description = component.description, !description.isEmpty { + return "\(component.signature) — \(description)" + } + return component.signature + } + let byName = Dictionary(spec.components.map { ($0.name, $0.spec) }, uniquingKeysWith: { $1 }) + + if let groups = spec.componentGroups, !groups.isEmpty { + var grouped: Set = [] + for group in groups { + lines.append("") + lines.append("### \(group.name)") + for name in group.components where !grouped.contains(name) { + guard let component = byName[name] else { continue } + grouped.insert(name) + lines.append(format(component)) + } + for note in group.notes ?? [] { lines.append(note) } + } + let ungrouped = spec.components.filter { !grouped.contains($0.name) } + if !ungrouped.isEmpty { + lines.append("") + lines.append("### Other") + for component in ungrouped { lines.append(format(component.spec)) } + } + } else { + lines.append("") + for component in spec.components { lines.append(format(component.spec)) } + } + return lines.joined(separator: "\n") +} + +// MARK: - Assembly + +/// Generates the system prompt that teaches a model OpenUI Lang and the +/// library's components. Matches lang-core's `generatePrompt`. +public func generatePrompt(_ spec: PromptSpec) -> String { + let rootName = spec.root ?? "Root" + let options = spec.options + let hasTools = !(options.tools ?? []).isEmpty + let toolCalls = options.toolCalls ?? hasTools + let bindings = options.bindings ?? toolCalls + let supportsExpressions = toolCalls || bindings + let usesActionExpression = spec.components.contains { + $0.spec.signature.contains("ActionExpression") + } + + var parts: [String] = [] + parts.append(options.preamble ?? defaultPreamble) + parts.append("") + parts.append(syntaxRules(rootName, supportsExpressions: supportsExpressions, bindings: bindings)) + parts.append("") + parts.append( + componentSignatures( + spec, toolCalls: toolCalls, bindings: bindings, usesActionExpression: usesActionExpression)) + + if supportsExpressions { + parts.append("") + parts.append(builtinFunctionsSection()) + } + if toolCalls { + parts.append("") + parts.append(querySection) + parts.append("") + parts.append(mutationSection) + } + if usesActionExpression { + parts.append("") + parts.append(actionSection(toolCalls: toolCalls, bindings: bindings)) + } + if toolCalls && bindings { + parts.append("") + parts.append(interactiveFiltersSection) + } + if toolCalls { + parts.append("") + parts.append(toolWorkflowSection) + } + if let tools = options.tools, !tools.isEmpty { + parts.append("") + parts.append(renderToolsSection(tools)) + } + parts.append("") + parts.append(streamingRules(rootName, supportsExpressions: supportsExpressions)) + + let examples = (options.examples ?? []) + (options.toolExamples ?? []) + if !examples.isEmpty { + parts.append("") + parts.append("## Examples") + parts.append("") + for example in examples { + parts.append(example) + parts.append("") + } + } + if options.editMode == true { + parts.append("") + parts.append(editModeSection) + } + if options.inlineMode == true { + parts.append("") + parts.append(inlineModeSection) + } + parts.append(importantRules(rootName, toolCalls: toolCalls, bindings: bindings)) + if let rules = options.additionalRules, !rules.isEmpty { + parts.append("") + for rule in rules { parts.append("- \(rule)") } + } + return parts.joined(separator: "\n") +} diff --git a/packages/swift-lang/Sources/OpenUILang/QueryManager.swift b/packages/swift-lang/Sources/OpenUILang/QueryManager.swift new file mode 100644 index 0000000000..f7b05d592a --- /dev/null +++ b/packages/swift-lang/Sources/OpenUILang/QueryManager.swift @@ -0,0 +1,516 @@ +import Foundation + +/// Calls the tools behind `Query(...)` and `Mutation(...)` statements: MCP, +/// REST, GraphQL or anything else. +public protocol ToolProvider: Sendable { + func callTool(_ name: String, arguments: OpenUIObject) async throws -> OpenUIValue +} + +/// Thrown when a tool name has no handler. +public struct ToolNotFoundError: Error, CustomStringConvertible { + public var toolName: String + public var availableTools: [String] + + public init(toolName: String, availableTools: [String] = []) { + self.toolName = toolName + self.availableTools = availableTools + } + + public var description: String { + let available = availableTools.isEmpty ? "(none)" : availableTools.joined(separator: ", ") + return "[openui] No handler for tool \"\(toolName)\". Available: \(available)" + } +} + +/// A tool call that returned an MCP error result. +public struct McpToolError: Error, CustomStringConvertible { + public var toolErrorText: String + + public init(toolErrorText: String) { + self.toolErrorText = toolErrorText + } + + public var description: String { + "MCP tool error: \(toolErrorText.isEmpty ? "Unknown error" : toolErrorText)" + } +} + +/// Unwraps an MCP `callTool` result (`{ content, structuredContent, isError }`): +/// throws `McpToolError` for error results, prefers `structuredContent`, then +/// text content parsed as JSON (or returned as text). +public func extractToolResult(_ result: OpenUIValue) throws -> OpenUIValue { + let textParts = (result["content"].arrayValue ?? []) + .filter { $0["type"].stringValue == "text" } + .map { $0["text"].stringValue ?? "" } + if result["isError"].isTruthy { + let errorText = textParts.joined(separator: "\n") + throw McpToolError(toolErrorText: errorText.isEmpty ? "Unknown error" : errorText) + } + if !result["structuredContent"].isNullish { return result["structuredContent"] } + if !textParts.isEmpty { + let text = textParts.joined() + return (try? JSON.parse(text)) ?? .string(text) + } + return .null +} + +/// A thrown error's message, as lang-core reads `err.message`: a +/// `LocalizedError`'s description, a Cocoa error's localized description +/// (its debug form reads `Error Domain=… Code=0 "(null)"`), or the error as +/// text, which for this package's errors is their description. +func errorMessage(_ error: Error) -> String { + if let description = (error as? LocalizedError)?.errorDescription { return description } + if type(of: error) is NSError.Type { return (error as NSError).localizedDescription } + return "\(error)" +} + +/// A tool provider backed by a map of async handlers. +public struct FunctionToolProvider: ToolProvider { + public typealias Handler = @Sendable (OpenUIObject) async throws -> OpenUIValue + + private let handlers: [String: Handler] + + public init(_ handlers: [String: Handler]) { + self.handlers = handlers + } + + public func callTool(_ name: String, arguments: OpenUIObject) async throws -> OpenUIValue { + guard let handler = handlers[name] else { + throw ToolNotFoundError(toolName: name, availableTools: handlers.keys.sorted()) + } + return try await handler(arguments) + } +} + +/// A tool provider for an MCP client, like passing one to react-lang's +/// Renderer as its `toolProvider`. `callTool` returns the client's raw result +/// (`{ content, structuredContent, isError }`); the provider unwraps it with +/// `extractToolResult`, so error results become `McpToolError`s, which +/// `onError` reports as `mcp-error`. +/// +/// It takes a closure rather than a client type, so any MCP client works +/// without this package depending on one: convert the client's result to an +/// `OpenUIValue` (for a JSON-RPC result, `JSON.parse` its text). +public struct McpToolProvider: ToolProvider { + public typealias CallTool = + @Sendable (_ name: String, _ arguments: OpenUIObject) async throws -> OpenUIValue + + private let call: CallTool + + public init(callTool: @escaping CallTool) { + call = callTool + } + + public func callTool(_ name: String, arguments: OpenUIObject) async throws -> OpenUIValue { + try extractToolResult(await call(name, arguments)) + } +} + +/// A Query statement evaluated against the current state. +public struct QueryNode: Sendable { + public var statementId: String + public var toolName: String + public var args: OpenUIValue + public var defaults: OpenUIValue + public var refreshInterval: Double? + /// Values of the `$state` the query depends on; part of the cache key. + public var deps: OpenUIObject? + public var complete: Bool +} + +public struct MutationNode: Sendable { + public var statementId: String + public var toolName: String +} + +/// Query results and status, published to observers when anything changes. +public struct QuerySnapshot: Sendable, Equatable { + /// Result per Query statement (data, previous data, or defaults) and per + /// Mutation statement (`{ status, data?, error? }`). + public var results: OpenUIObject = OpenUIObject() + public var loading: [String] = [] + public var refetching: [String] = [] + public var errors: [OpenUIError] = [] +} + +private func stableStringify(_ value: OpenUIValue) -> String { + func normalize(_ value: OpenUIValue) -> OpenUIValue { + switch value { + case .undefined: return "__undefined__" + case .number(let number) where number.isNaN: return "__NaN__" + case .number(let number) where number == .infinity: return "__Inf__" + case .number(let number) where number == -.infinity: return "__-Inf__" + case .array(let items): return .array(items.map(normalize)) + case .object(let object): + return .object(OpenUIObject(object.keys.sorted().map { ($0, normalize(object[$0]!)) })) + default: return value + } + } + return JSON.stringify(normalize(value.jsonRepresentation)) +} + +private func cacheKey(_ toolName: String, _ args: OpenUIValue, _ deps: OpenUIObject?) -> String { + let depsKey = deps.map { "::" + stableStringify(.object($0)) } ?? "" + return toolName + "::" + stableStringify(args) + depsKey +} + +/// Fetches, caches and refreshes Query results and runs Mutations. Ports +/// lang-core's `createQueryManager`. +@MainActor +public final class QueryManager { + private struct QueryEntry { + var toolName: String + var args: OpenUIValue + var defaults: OpenUIValue + var cacheKey: String + var prevCacheKey: String? + var loading = false + var everFetched = false + var refreshInterval: Double = 0 + var timer: Task? + var needsRefetch = false + var error: OpenUIError? + } + + private struct MutationEntry { + var toolName: String + var result: OpenUIValue + var error: OpenUIError? + } + + private struct CacheEntry { + var data: OpenUIValue = .undefined + var inFlight = false + } + + private let toolProvider: (any ToolProvider)? + private var queryOrder: [String] = [] + private var queries: [String: QueryEntry] = [:] + private var mutationOrder: [String] = [] + private var mutations: [String: MutationEntry] = [:] + private var cache: [String: CacheEntry] = [:] + private var listeners: [Int: () -> Void] = [:] + private var nextListenerId = 0 + private var disposed = false + private var generation = 0 + + public private(set) var snapshot = QuerySnapshot() + + public init(toolProvider: (any ToolProvider)?) { + self.toolProvider = toolProvider + } + + // MARK: Snapshot + + @discardableResult + private func rebuildSnapshot() -> Bool { + var out = QuerySnapshot() + for id in queryOrder { + guard let query = queries[id] else { continue } + out.results[id] = resultValue(query) + if query.loading { + out.loading.append(id) + if query.everFetched { out.refetching.append(id) } + } + if let error = query.error { out.errors.append(error) } + } + for id in mutationOrder { + guard let mutation = mutations[id] else { continue } + out.results[id] = mutation.result + if let error = mutation.error { out.errors.append(error) } + } + guard out != snapshot else { return false } + snapshot = out + return true + } + + private func resultValue(_ query: QueryEntry) -> OpenUIValue { + if let data = cache[query.cacheKey]?.data, data != .undefined { return data } + if let previous = query.prevCacheKey, let data = cache[previous]?.data, data != .undefined { + return data + } + return query.defaults + } + + private func notify() { + for listener in listeners.sorted(by: { $0.key < $1.key }).map(\.value) { listener() } + } + + @discardableResult + public func subscribe(_ listener: @escaping () -> Void) -> () -> Void { + let id = nextListenerId + nextListenerId += 1 + listeners[id] = listener + return { [weak self] in self?.listeners[id] = nil } + } + + // MARK: Queries + + /// Starts a fetch. Like the JavaScript async function, everything before the + /// tool call runs synchronously, so the query is marked loading right away. + private func startFetch(_ fetchKey: String, _ statementId: String) { + guard let toolProvider, let query = queries[statementId] else { return } + let toolName = query.toolName + let args = query.args.objectValue ?? OpenUIObject() + cache[fetchKey, default: CacheEntry()].inFlight = true + queries[statementId]?.loading = true + rebuildSnapshot() + notify() + Task { + let outcome: Result + do { + outcome = .success(try await toolProvider.callTool(toolName, arguments: args)) + } catch { + outcome = .failure(error) + } + finishFetch(fetchKey, statementId, toolName, outcome) + } + } + + private func finishFetch( + _ fetchKey: String, _ statementId: String, _ toolName: String, + _ outcome: Result + ) { + switch outcome { + case .success(let data): + if disposed { return } + guard queries[statementId]?.cacheKey == fetchKey else { + cache[fetchKey]?.inFlight = false + return + } + cache[fetchKey]?.data = data.isNullish ? .null : data + queries[statementId]?.everFetched = true + queries[statementId]?.error = nil + if let previous = queries[statementId]?.prevCacheKey, previous != fetchKey { + queries[statementId]?.prevCacheKey = nil + cleanupCacheEntry(previous) + } + case .failure(let error): + if queries[statementId]?.cacheKey == fetchKey { + queries[statementId]?.error = queryError( + error, toolName: toolName, statementId: statementId) + } + } + + cache[fetchKey]?.inFlight = false + if queries[statementId]?.cacheKey == fetchKey { + queries[statementId]?.loading = false + if rebuildSnapshot() { notify() } + if queries[statementId]?.needsRefetch == true { + queries[statementId]?.needsRefetch = false + if let key = queries[statementId]?.cacheKey { startFetch(key, statementId) } + } + } else if rebuildSnapshot() { + notify() + } + } + + private func queryError(_ error: Error, toolName: String, statementId: String) -> OpenUIError { + if let error = error as? ToolNotFoundError { + return OpenUIError( + source: .query, code: "tool-not-found", message: "Query tool \"\(toolName)\" not found", + statementId: statementId, component: "Query", toolName: toolName, + hint: error.availableTools.isEmpty + ? nil : "Available tools: \(error.availableTools.joined(separator: ", "))") + } + if let error = error as? McpToolError { + return OpenUIError( + source: .query, code: "mcp-error", + message: "Query \"\(toolName)\" returned an error: \(error.toolErrorText)", + statementId: statementId, component: "Query", toolName: toolName) + } + return OpenUIError( + source: .query, code: "tool-error", + message: "Query \"\(toolName)\" failed: \(errorMessage(error))", + statementId: statementId, component: "Query", toolName: toolName) + } + + private func cleanupCacheEntry(_ key: String) { + for query in queries.values where query.cacheKey == key || query.prevCacheKey == key { return } + cache[key] = nil + } + + /// Syncs the active Query statements: drops removed ones, fetches new or + /// changed ones, and (re)schedules refresh intervals. + public func evaluateQueries(_ nodes: [QueryNode]) { + if disposed { return } + let activeIds = Set(nodes.map(\.statementId)) + for id in queryOrder where !activeIds.contains(id) { + guard let query = queries.removeValue(forKey: id) else { continue } + query.timer?.cancel() + cleanupCacheEntry(query.cacheKey) + if let previous = query.prevCacheKey { cleanupCacheEntry(previous) } + } + queryOrder.removeAll { !activeIds.contains($0) } + + for node in nodes where node.complete { + let key = cacheKey(node.toolName, node.args, node.deps) + if var existing = queries[node.statementId] { + if existing.cacheKey != key { existing.prevCacheKey = existing.cacheKey } + existing.toolName = node.toolName + existing.args = node.args + existing.defaults = node.defaults + existing.cacheKey = key + queries[node.statementId] = existing + } else { + queries[node.statementId] = QueryEntry( + toolName: node.toolName, args: node.args, defaults: node.defaults, cacheKey: key) + queryOrder.append(node.statementId) + } + + let entry = cache[key] + let hasSettledData = entry.map { $0.data != .undefined && !$0.inFlight } ?? false + if toolProvider != nil, !hasSettledData, entry?.inFlight != true { + startFetch(key, node.statementId) + } + + let interval = node.refreshInterval ?? 0 + if interval != queries[node.statementId]?.refreshInterval { + queries[node.statementId]?.timer?.cancel() + queries[node.statementId]?.timer = nil + if interval > 0 { + let statementId = node.statementId + // At most setInterval's longest delay (2³¹−1 ms): the interval comes + // from the response, and an infinite one can't be slept for. + let nanoseconds = UInt64(min(interval, 2_147_483.647) * 1_000_000_000) + queries[node.statementId]?.timer = Task { [weak self] in + while !Task.isCancelled { + try? await Task.sleep(nanoseconds: nanoseconds) + guard let self, !Task.isCancelled, !self.disposed, self.toolProvider != nil, + let current = self.queries[statementId] + else { return } + if self.cache[current.cacheKey]?.inFlight != true { + self.startFetch(current.cacheKey, statementId) + } + } + } + } + queries[node.statementId]?.refreshInterval = interval + } + } + if rebuildSnapshot() { notify() } + } + + /// The current value for a Query: fetched data, the previous result while a + /// new key loads, or the declared defaults. `null` for unknown statements. + public func result(_ statementId: String) -> OpenUIValue { + guard let query = queries[statementId] else { return .null } + return resultValue(query) + } + + public func isLoading(_ statementId: String) -> Bool { queries[statementId]?.loading ?? false } + + public var isAnyLoading: Bool { queries.values.contains { $0.loading } } + + /// Refetches the given queries (all when nil or empty). Queries already in + /// flight refetch once they settle. + public func invalidate(_ statementIds: [String]? = nil) { + if disposed || toolProvider == nil { return } + let targets = + (statementIds?.isEmpty == false) ? statementIds!.filter { queries[$0] != nil } : queryOrder + for id in targets { + guard let query = queries[id] else { continue } + if cache[query.cacheKey]?.inFlight == true { + queries[id]?.needsRefetch = true + } else { + startFetch(query.cacheKey, id) + } + } + } + + // MARK: Mutations + + public func registerMutations(_ nodes: [MutationNode]) { + let activeIds = Set(nodes.map(\.statementId)) + for id in mutationOrder where !activeIds.contains(id) { mutations[id] = nil } + mutationOrder.removeAll { !activeIds.contains($0) } + for node in nodes { + if let existing = mutations[node.statementId] { + if existing.toolName != node.toolName { + mutations[node.statementId] = MutationEntry( + toolName: node.toolName, result: ["status": "idle", "data": .null, "error": .null]) + } + } else { + mutations[node.statementId] = MutationEntry( + toolName: node.toolName, result: ["status": "idle"]) + mutationOrder.append(node.statementId) + } + } + if rebuildSnapshot() { notify() } + } + + /// Runs a mutation. Returns false when it fails, is already running, or + /// there is no tool provider; a failure halts the action's remaining steps. + public func fireMutation( + _ statementId: String, args: OpenUIObject, refreshQueryIds: [String]? = nil + ) async -> Bool { + guard !disposed, let toolProvider, let mutation = mutations[statementId], + mutation.result["status"].stringValue != "loading" + else { return false } + let currentGeneration = generation + mutations[statementId]?.result = ["status": "loading"] + rebuildSnapshot() + notify() + + var success = false + do { + let data = try await toolProvider.callTool(mutation.toolName, arguments: args) + if disposed || currentGeneration != generation { return false } + mutations[statementId]?.result = ["status": "success", "data": data] + mutations[statementId]?.error = nil + success = true + } catch { + if disposed || currentGeneration != generation { return false } + mutations[statementId]?.result = ["status": "error", "error": .string(errorMessage(error))] + mutations[statementId]?.error = mutationError( + error, toolName: mutation.toolName, statementId: statementId) + } + rebuildSnapshot() + notify() + if success, let refreshQueryIds, !refreshQueryIds.isEmpty { invalidate(refreshQueryIds) } + return success + } + + private func mutationError(_ error: Error, toolName: String, statementId: String) -> OpenUIError { + if let error = error as? ToolNotFoundError { + return OpenUIError( + source: .mutation, code: "tool-not-found", + message: "Mutation tool \"\(toolName)\" not found", statementId: statementId, + component: "Mutation", toolName: toolName, + hint: error.availableTools.isEmpty + ? nil : "Available tools: \(error.availableTools.joined(separator: ", "))") + } + if let error = error as? McpToolError { + return OpenUIError( + source: .mutation, code: "mcp-error", + message: "Mutation \"\(toolName)\" returned an error: \(error.toolErrorText)", + statementId: statementId, component: "Mutation", toolName: toolName) + } + return OpenUIError( + source: .mutation, code: "tool-error", + message: "Mutation \"\(toolName)\" failed: \(errorMessage(error))", + statementId: statementId, component: "Mutation", toolName: toolName) + } + + /// `{ status, data?, error? }` for a Mutation statement, or nil. + public func mutationResult(_ statementId: String) -> OpenUIValue? { + mutations[statementId]?.result + } + + public func activate() { disposed = false } + + public func dispose() { + disposed = true + generation += 1 + listeners.removeAll() + for id in queryOrder { + queries[id]?.timer?.cancel() + queries[id]?.timer = nil + queries[id]?.refreshInterval = 0 + queries[id]?.loading = false + queries[id]?.needsRefetch = false + } + mutations.removeAll() + mutationOrder.removeAll() + } +} diff --git a/packages/swift-lang/Sources/OpenUILang/Runtime.swift b/packages/swift-lang/Sources/OpenUILang/Runtime.swift new file mode 100644 index 0000000000..90fe9a4c66 --- /dev/null +++ b/packages/swift-lang/Sources/OpenUILang/Runtime.swift @@ -0,0 +1,408 @@ +/// What the parser and evaluator need to know about a component library. +public protocol ComponentLibrary { + /// Positional parameters per component, compiled from the library schema. + var paramMap: ParamMap { get } + /// The root component, used to pick the entry statement. + var rootName: String? { get } + /// Whether a prop binds two-way to `$state` (lang-core's `reactive()`). + func isReactiveProp(_ component: String, _ prop: String) -> Bool + /// Component names in library order, listed in error hints. + var componentNames: [String] { get } +} + +extension ComponentLibrary { + public var componentNames: [String] { paramMap.componentNames } +} + +/// The framework-independent runtime behind a renderer: streaming parse, +/// state store, prop evaluation, form fields and action execution. Ports the +/// logic of react-lang's `useOpenUIState`; UI layers observe it and render. +@MainActor +public final class OpenUIRuntime { + public let library: any ComponentLibrary + public let store = Store() + public let queries: QueryManager + + /// Called for actions the host handles (continue the conversation, open a URL, custom). + public var onAction: ((ActionEvent) -> Void)? + /// Called when state changes after initialization. + public var onStateUpdate: ((OpenUIObject) -> Void)? + /// Called whenever the parse result changes. + public var onParseResult: ((ParseResult?) -> Void)? + /// Called with structured errors an LLM can fix by changing the response: + /// parser validation errors with hints, a response without a root, and + /// failed Query/Mutation calls. Reported once streaming finishes, and with + /// `[]` when they clear or the next response starts streaming, so a + /// correction loop gets a clean signal. Matches react-lang's `onError`. + public var onError: (([OpenUIError]) -> Void)? + + public private(set) var parseResult: ParseResult? + public private(set) var isStreaming = false + + private let streamParser: StreamParser + private let initialState: OpenUIObject? + private var storeInitKey: String? + private var initializingStore = false + private var unsubscribers: [() -> Void] = [] + private var response: String? + private var hasParsed = false + /// The errors last passed to `onError`; nil when nothing is reported. + private var reportedErrors: [OpenUIError]? + private var streamObservation = StreamObservation() + + /// Whether to publish `react-lang:stream` events on `Observability.shared` + /// (react-lang's `publishObservability`). + public var publishesObservability = true + + public init( + library: some ComponentLibrary, initialState: OpenUIObject? = nil, + toolProvider: (any ToolProvider)? = nil + ) { + self.library = library + self.initialState = initialState + self.streamParser = StreamParser(library.paramMap, rootName: library.rootName) + self.queries = QueryManager(toolProvider: toolProvider) + unsubscribers.append( + store.subscribe { [weak self] in + guard let self, !self.initializingStore else { return } + self.onStateUpdate?(self.store.snapshot) + }) + unsubscribers.append( + queries.subscribe { [weak self] in + self?.reportErrors() + self?.publishStream() + }) + } + + /// Stops query refreshes and drops listeners. + public func dispose() { + for unsubscribe in unsubscribers { unsubscribe() } + unsubscribers.removeAll() + queries.dispose() + } + + // MARK: Parsing + + /// Parses `response` so it can be rendered right away, without telling the + /// host or starting queries. The next `update` reports it and starts them, + /// as if it were the first. + public func preload(response: String?, isStreaming: Bool) { + guard !hasParsed else { return } + self.isStreaming = isStreaming + self.response = response + guard let response, !response.isEmpty else { return } + parseResult = streamParser.set(response) + initializeStoreIfNeeded() + } + + /// Feeds the latest full response text. Appended text parses incrementally. + public func update(response: String?, isStreaming: Bool) { + self.isStreaming = isStreaming + defer { + reportErrors() + publishStream() + } + let changed = !hasParsed || response != self.response + hasParsed = true + self.response = response + guard let response, !response.isEmpty else { + parseResult = nil + if changed { onParseResult?(nil) } + return + } + if changed { + parseResult = streamParser.set(response) + onParseResult?(parseResult) + } + initializeStoreIfNeeded() + if !isStreaming { syncQueries() } + } + + /// Seeds the store from `$state` declarations and `initialState` whenever + /// either changes. Values the user already changed are kept. + private func initializeStoreIfNeeded() { + let declarations = parseResult?.stateDeclarations ?? OpenUIObject() + let key = + JSON.stringify(.object(declarations)) + "::" + + (initialState.map { JSON.stringify(.object($0)) } ?? "") + guard key != storeInitKey else { return } + storeInitKey = key + + initializingStore = true + defer { initializingStore = false } + var bindingDefaults = OpenUIObject() + for (key, value) in initialState ?? OpenUIObject() { + if key.hasPrefix("$") { + bindingDefaults[key] = value + } else { + store.set(key, value) + } + } + store.initialize(defaults: declarations, persisted: bindingDefaults) + } + + // MARK: Errors + + /// The errors `onError` reports for the current response and query state. + /// Ports react-lang's `useOpenUIErrors`. Prop evaluation and SwiftUI views + /// can't throw, so there are no `runtime-error` or `render-error` entries. + public var errors: [OpenUIError] { + var errors: [OpenUIError] = [] + if let response, !response.isEmpty, parseResult?.root == nil { + errors.append( + OpenUIError( + source: .parser, code: "parse-failed", + message: "Code parsed but produced no renderable root component", + hint: + "The entire response must be valid openui-lang code starting with root = \(library.rootName ?? "Root")(...)" + )) + } + errors += (parseResult?.meta.errors ?? []).map(withHint) + errors += queries.snapshot.errors + return errors + } + + private func reportErrors() { + guard let onError else { return } + if isStreaming { + if reportedErrors != nil { + reportedErrors = nil + onError([]) + } + return + } + let errors = errors + guard errors != reportedErrors else { return } + reportedErrors = errors + onError(errors) + } + + // MARK: Observability + + /// Publishes the stream lifecycle like react-lang's + /// `useStreamingObservability`: each streamed chunk, then the settled + /// response with its errors (as an error event when there are any). + private func publishStream() { + guard publishesObservability else { return } + let errors = isStreaming ? [] : self.errors + let errorJSON = errors.map(\.jsonRepresentation) + guard + let update = streamObservation.advance( + isStreaming: isStreaming, response: response, + settledErrorKey: isStreaming ? nil : JSON.stringify(.array(errorJSON))) + else { return } + let timing = streamObservation.timing() + guard Observability.shared.hasListeners else { return } + + var detail: OpenUIObject = [ + "id": .string(update.id), "kind": .string(streamEventKind), + "phase": .string(update.phase.rawValue), "updateIndex": .number(Double(update.updateIndex)), + "response": response.map(OpenUIValue.string) ?? .null, + "responseLength": .number(Double(response?.utf16.count ?? 0)), + ] + if let meta = parseResult?.meta { + detail["parser"] = [ + "incomplete": .bool(meta.incomplete), + "unresolved": .array(meta.unresolved.map { .string($0) }), + "orphaned": .array(meta.orphaned.map { .string($0) }), + "statementCount": .number(Double(meta.statementCount)), + ] + } + let settled = update.phase == .settled + if settled { + detail["errors"] = .array(errorJSON) + detail["errorCount"] = .number(Double(errors.count)) + } + for (key, value) in timing { detail[key] = value } + detail["message"] = .string( + !settled + ? "OpenUI Lang is streaming" + : errors.isEmpty + ? "OpenUI Lang settled" + : "OpenUI Lang settled with \(errors.count) error\(errors.count == 1 ? "" : "s")") + Observability.shared.emit(settled && !errors.isEmpty ? .error : .info, detail) + } + + /// A validation error as an `OpenUIError`, with the fix hint lang-core's + /// `enrichErrors` adds. + private func withHint(_ error: ValidationError) -> OpenUIError { + var result = OpenUIError( + source: .parser, code: error.code.rawValue, message: error.message, + statementId: error.statementId, component: error.component, + path: error.path.isEmpty ? nil : error.path) + switch error.code { + case .unknownComponent where !library.componentNames.isEmpty: + result.hint = "Available components: \(library.componentNames.joined(separator: ", "))" + case .missingRequired, .nullRequired: + result.hint = library.paramMap[error.component].map { params in + let names = params.map { $0.required ? "\($0.name)*" : $0.name } + return "Signature: \(error.component)(\(names.joined(separator: ", "))) — * marks required" + } + case .inlineReserved: + result.hint = "Declare as a top-level statement: myVar = \(error.component)(...)" + default: + break + } + return result + } + + // MARK: Evaluation + + /// Reads state for expressions (form fields store `{ value, componentType }`) + /// and Query/Mutation results as of now. + public var evaluationContext: EvaluationContext { + let results = queries.snapshot.results + return EvaluationContext( + getState: { [store] name in unwrapFieldValue(store.get(name)) }, + resolveRef: { name in results[name] ?? .null }) + } + + public var schemaContext: SchemaContext { + SchemaContext(isReactiveProp: library.isReactiveProp) + } + + /// The parsed root with every prop evaluated against the current state. + public func evaluatedRoot() -> ElementNode? { + guard let root = parseResult?.root else { return nil } + return evaluateElementProps(root, evaluationContext, schemaContext) + } + + // MARK: Fields + + public func fieldValue(form: String?, name: String) -> OpenUIValue { + guard let form else { return unwrapFieldValue(store.get(name)) } + guard case .object(let formData) = store.get(form) else { return .undefined } + return unwrapFieldValue(formData[name] ?? .undefined) + } + + public func setFieldValue( + form: String?, componentType: String?, name: String, value: OpenUIValue, + notifyHost: Bool = true + ) { + var wrapped: OpenUIObject = ["value": value] + wrapped["componentType"] = componentType.map { .string($0) } ?? .undefined + if let form { + var formData = store.get(form).objectValue ?? OpenUIObject() + formData[name] = .object(wrapped) + store.set(form, .object(formData)) + } else { + store.set(name, .object(wrapped)) + } + if notifyHost { onStateUpdate?(store.snapshot) } + } + + /// The field a component reads and writes for `name`, honouring `$state` bindings. + public func stateField(name: String, binding: OpenUIValue, form: String?) -> StateField { + resolveStateField( + name: name, bindingValue: binding, store: store, evaluationContext: evaluationContext, + getField: { [weak self] field in self?.fieldValue(form: form, name: field) ?? .undefined }, + setField: { [weak self] field, value in + self?.setFieldValue(form: form, componentType: nil, name: field, value: value) + }) + } + + /// The state sent along with an action: the named form, or everything. + public func formPayload(_ form: String?) -> OpenUIObject { + if let form, case .object = store.get(form) { return [form: store.get(form)] } + return store.snapshot + } + + // MARK: Actions + + /// Runs a component's action. `action` is an evaluated `Action([...])` plan, + /// a legacy `{ type, params }` object, or nil (continue the conversation + /// with the label as the message). + public func triggerAction(_ userMessage: String, form: String? = nil, action: OpenUIValue? = nil) + async + { + let payload = formPayload(form) + + if case .object(let legacy)? = action { + var params = legacy["params"]?.objectValue ?? OpenUIObject() + if let url = legacy["url"], !url.isNullish { params["url"] = url } + if let context = legacy["context"], !context.isNullish { params["context"] = context } + let type = legacy["type"]?.stringValue.flatMap { $0.isEmpty ? nil : $0 } + onAction?( + ActionEvent( + type: type ?? BuiltinActionType.continueConversation, params: params, + humanFriendlyMessage: userMessage, formState: payload, formName: form)) + return + } + + guard case .actionPlan(let plan)? = action else { + onAction?( + ActionEvent( + type: BuiltinActionType.continueConversation, humanFriendlyMessage: userMessage, + formState: payload, formName: form)) + return + } + + for step in plan.steps { + switch step { + case .run(let statementId, let refType): + if refType == .mutation { + let argsAST = parseResult?.mutationStatements.first { $0.statementId == statementId }? + .argsAST + let args = argsAST.map { evaluate($0, evaluationContext) }?.objectValue ?? OpenUIObject() + // A failed mutation halts the remaining steps. + guard await queries.fireMutation(statementId, args: args) else { return } + } else { + queries.invalidate([statementId]) + } + case .continueConversation(let message, let context): + onAction?( + ActionEvent( + type: BuiltinActionType.continueConversation, + params: context.map { ["context": .string($0)] } ?? OpenUIObject(), + humanFriendlyMessage: message, formState: payload, formName: form)) + case .openUrl(let url): + onAction?( + ActionEvent( + type: BuiltinActionType.openUrl, params: ["url": .string(url)], + humanFriendlyMessage: "", formState: payload, formName: form)) + case .set(let target, let valueAST): + store.set(target, evaluate(valueAST, evaluationContext)) + case .reset(let targets): + let declarations = parseResult?.stateDeclarations ?? OpenUIObject() + for target in targets { + let declared = declarations[target] ?? .null + store.set(target, declared.isNullish ? .null : declared) + } + } + } + } + + // MARK: Queries + + /// Evaluates Query/Mutation statements against the current state and hands + /// them to the query manager. Runs once the stream has finished. + public func syncQueries() { + guard let result = parseResult, !isStreaming else { return } + let context = evaluationContext + let snapshot = store.snapshot + queries.evaluateQueries( + result.queryStatements.map { query in + var deps = OpenUIObject() + for dep in query.deps ?? [] { deps[dep] = snapshot[dep] ?? .undefined } + return QueryNode( + statementId: query.statementId, + toolName: query.toolAST.map { evaluate($0, context) }?.stringValue ?? "", + args: query.argsAST.map { evaluate($0, context) } ?? .null, + defaults: query.defaultsAST.map { evaluate($0, context) } ?? .null, + refreshInterval: query.refreshAST.map { evaluate($0, context) }?.numberValue, + deps: deps.isEmpty ? nil : deps, complete: query.complete) + }) + queries.registerMutations( + result.mutationStatements.map { mutation in + MutationNode( + statementId: mutation.statementId, + toolName: mutation.toolAST.map { evaluate($0, context) }?.stringValue ?? "") + }) + } +} + +/// Form fields store `{ value, componentType }`; expressions see the raw value. +func unwrapFieldValue(_ value: OpenUIValue) -> OpenUIValue { + if case .object(let object) = value, let inner = object["value"] { return inner } + return value +} diff --git a/packages/swift-lang/Sources/OpenUILang/Statements.swift b/packages/swift-lang/Sources/OpenUILang/Statements.swift new file mode 100644 index 0000000000..b3251200be --- /dev/null +++ b/packages/swift-lang/Sources/OpenUILang/Statements.swift @@ -0,0 +1,136 @@ +/// A raw `identifier = expression` statement before expression parsing. +struct RawStatement: Sendable { + var id: String + /// Token type of the identifier, used to classify the statement. + var idTokenType: TokenType + var tokens: [Token] +} + +/// Closes unterminated strings and brackets so partial, streaming input parses +/// without syntax errors. +public func autoClose(_ input: String) -> (text: String, wasIncomplete: Bool) { + var stack: [UInt16] = [] + var inString: UInt16? = nil + var escaped = false + + for c in input.utf16 { + if escaped { + escaped = false + continue + } + if c == UInt16(ascii: "\\") && inString != nil { + escaped = true + continue + } + if let quote = inString { + if c == quote { inString = nil } + continue + } + switch c { + case UInt16(ascii: "\""), UInt16(ascii: "'"): + inString = c + case UInt16(ascii: "("), UInt16(ascii: "["), UInt16(ascii: "{"): + stack.append(c) + case UInt16(ascii: ")"): + if stack.last == UInt16(ascii: "(") { stack.removeLast() } + case UInt16(ascii: "]"): + if stack.last == UInt16(ascii: "[") { stack.removeLast() } + case UInt16(ascii: "}"): + if stack.last == UInt16(ascii: "{") { stack.removeLast() } + default: + break + } + } + + let wasIncomplete = inString != nil || !stack.isEmpty + guard wasIncomplete else { return (input, false) } + + var out = input + if let quote = inString { + if escaped { out += "\\" } + out += String(decoding: [quote], as: UTF16.self) + } + for opener in stack.reversed() { + switch opener { + case UInt16(ascii: "("): out += ")" + case UInt16(ascii: "["): out += "]" + default: out += "}" + } + } + return (out, true) +} + +/// Splits the token stream into `identifier = expression` statements. +/// +/// Statements end at newlines at bracket depth 0. A newline followed by `?` +/// (or by `:` inside a ternary) continues a multi-line ternary. Lines without +/// an identifier and `=` are skipped. +func splitStatements(_ tokens: [Token]) -> [RawStatement] { + var statements: [RawStatement] = [] + var pos = 0 + + func skipLine() { + while pos < tokens.count && tokens[pos].type != .newline && tokens[pos].type != .eof { + pos += 1 + } + } + + while pos < tokens.count { + while pos < tokens.count && tokens[pos].type == .newline { pos += 1 } + if pos >= tokens.count || tokens[pos].type == .eof { break } + + let token = tokens[pos] + guard token.type == .ident || token.type == .type || token.type == .stateVar else { + skipLine() + continue + } + let id = token.string ?? "" + let idTokenType = token.type + pos += 1 + + guard pos < tokens.count, tokens[pos].type == .equals else { + skipLine() + continue + } + pos += 1 + + var expr: [Token] = [] + var depth = 0 + var ternaryDepth = 0 + while pos < tokens.count && tokens[pos].type != .eof { + let type = tokens[pos].type + if type == .newline && depth <= 0 && ternaryDepth <= 0 { + var peek = pos + 1 + while peek < tokens.count && tokens[peek].type == .newline { peek += 1 } + let nextType = peek < tokens.count ? tokens[peek].type : .eof + if nextType == .question || (nextType == .colon && ternaryDepth > 0) { + pos += 1 + continue + } + break + } + if type == .newline { + pos += 1 + continue + } + if type == .lParen || type == .lBrack || type == .lBrace { + depth += 1 + } else if (type == .rParen || type == .rBrack || type == .rBrace) && depth > 0 { + depth -= 1 + } else if type == .question && depth == 0 { + // Colons inside {} are object key separators, so only depth-0 `?`/`:` count. + ternaryDepth += 1 + } else if type == .colon && depth == 0 && ternaryDepth > 0 { + ternaryDepth -= 1 + } + expr.append(tokens[pos]) + pos += 1 + } + + if !expr.isEmpty { + statements.append(RawStatement(id: id, idTokenType: idTokenType, tokens: expr)) + } + } + + return statements +} diff --git a/packages/swift-lang/Sources/OpenUILang/Store.swift b/packages/swift-lang/Sources/OpenUILang/Store.swift new file mode 100644 index 0000000000..b7d168e2b3 --- /dev/null +++ b/packages/swift-lang/Sources/OpenUILang/Store.swift @@ -0,0 +1,94 @@ +import Foundation + +/// The reactive state store: `$bindings` as top-level keys, form fields nested +/// under their form name. +public final class Store { + private var state = OpenUIObject() + private var listeners: [UUID: () -> Void] = [:] + private var listenerOrder: [UUID] = [] + + public init() {} + + public func get(_ name: String) -> OpenUIValue { + state[name] ?? .undefined + } + + /// Sets a value and notifies listeners, unless the value is unchanged (plain + /// objects compare shallowly, as form data is replaced on every edit). + public func set(_ name: String, _ value: OpenUIValue) { + let existing = state[name] ?? .undefined + if existing.isSameValue(value) { return } + if case .object(let next) = value, case .object(let current) = existing, + next.keys.count == current.keys.count, + next.keys.allSatisfy({ (next[$0] ?? .undefined).isSameValue(current[$0] ?? .undefined) }) + { + return + } + state[name] = value + notify() + } + + /// Registers a listener; returns a function that removes it. + @discardableResult + public func subscribe(_ listener: @escaping () -> Void) -> () -> Void { + let id = UUID() + listeners[id] = listener + listenerOrder.append(id) + return { [weak self] in + self?.listeners[id] = nil + self?.listenerOrder.removeAll { $0 == id } + } + } + + public var snapshot: OpenUIObject { state } + + /// Applies persisted values, then defaults for keys that don't exist yet. + /// Values the user already changed are never overwritten or deleted: while + /// streaming, declarations can briefly disappear and deleting would lose input. + public func initialize(defaults: OpenUIObject, persisted: OpenUIObject) { + for (key, value) in persisted { state[key] = value } + for (key, value) in defaults where !state.contains(key) { state[key] = value } + notify() + } + + public func dispose() { + state = OpenUIObject() + listeners.removeAll() + listenerOrder.removeAll() + } + + private func notify() { + for id in listenerOrder { listeners[id]?() } + } +} + +/// A form field or `$binding` a component reads and writes. +public struct StateField { + public var name: String + public var value: OpenUIValue + public var setValue: (OpenUIValue) -> Void + /// True when the field is bound to a `$state` variable. + public var isReactive: Bool +} + +/// Resolves the field a component should read and write: a reactive binding +/// when the prop is bound to `$state`, otherwise the form field `name`. +public func resolveStateField( + name: String, bindingValue: OpenUIValue, store: Store?, evaluationContext: EvaluationContext?, + getField: @escaping (String) -> OpenUIValue, + setField: @escaping (String, OpenUIValue) -> Void +) -> StateField { + if case .reactiveAssign(let assign) = bindingValue, let store, let evaluationContext { + return StateField( + name: name, value: store.get(assign.target), + setValue: { newValue in + var context = evaluationContext + context.extraScope = ["$value": newValue] + store.set(assign.target, evaluate(assign.expr, context)) + }, isReactive: true) + } + let current = getField(name) + return StateField( + name: name, value: current.isNullish ? bindingValue : current, + setValue: { setField(name, $0) }, isReactive: false) +} diff --git a/packages/swift-lang/Sources/OpenUILang/Types.swift b/packages/swift-lang/Sources/OpenUILang/Types.swift new file mode 100644 index 0000000000..f8b121b692 --- /dev/null +++ b/packages/swift-lang/Sources/OpenUILang/Types.swift @@ -0,0 +1,223 @@ +/// A resolved component node produced by the parser: positional arguments +/// mapped to named props through the library schema. +public struct ElementNode: Sendable, Equatable { + /// Source statement name (`header` in `header = TextContent(...)`); `nil` for inline components. + public var statementId: String? + /// Component name as defined in the library. + public var typeName: String + public var props: OpenUIObject + /// True while the parser hasn't received all tokens yet. + public var partial: Bool + /// False when every prop is a static literal, so evaluation can be skipped. + /// `nil` is treated as dynamic. + public var hasDynamicProps: Bool? + + public init( + statementId: String? = nil, typeName: String, props: OpenUIObject, partial: Bool, + hasDynamicProps: Bool? = nil + ) { + self.statementId = statementId + self.typeName = typeName + self.props = props + self.partial = partial + self.hasDynamicProps = hasDynamicProps + } +} + +public enum ValidationErrorCode: String, Sendable, Equatable { + case missingRequired = "missing-required" + case nullRequired = "null-required" + case unknownComponent = "unknown-component" + case inlineReserved = "inline-reserved" + case excessArgs = "excess-args" + case typeMismatch = "type-mismatch" +} + +/// A prop validation error. Components with missing required props are dropped +/// from the tree and reported here. +public struct ValidationError: Sendable, Equatable { + public var code: ValidationErrorCode + public var component: String + /// JSON Pointer within the props object, e.g. `/title`. + public var path: String + public var message: String + public var statementId: String? +} + +public enum OpenUIErrorSource: String, Sendable, Equatable { + case parser + case runtime + case query + case mutation +} + +/// A structured, LLM-friendly error from the OpenUI Lang pipeline. +public struct OpenUIError: Sendable, Equatable { + public var source: OpenUIErrorSource + /// A `ValidationErrorCode` raw value, or one of `runtime-error`, `render-error`, + /// `parse-exception`, `parse-failed`, `tool-not-found`, `tool-error`, `mcp-error`. + public var code: String + public var message: String + public var statementId: String? + public var component: String? + public var path: String? + public var toolName: String? + public var hint: String? + + public init( + source: OpenUIErrorSource, code: String, message: String, statementId: String? = nil, + component: String? = nil, path: String? = nil, toolName: String? = nil, hint: String? = nil + ) { + self.source = source + self.code = code + self.message = message + self.statementId = statementId + self.component = component + self.path = path + self.toolName = toolName + self.hint = hint + } +} + +/// Built-in action types delivered to the host app. +public enum BuiltinActionType { + public static let continueConversation = "continue_conversation" + public static let openUrl = "open_url" +} + +/// One step of an `ActionPlan`. Step type names match lang-core's `ACTION_STEPS`. +public enum ActionStep: Sendable, Equatable { + case run(statementId: String, refType: RuntimeRefType) + case continueConversation(message: String, context: String?) + case openUrl(url: String) + /// `valueAST` is evaluated when the action fires, not when it is built. + case set(target: String, valueAST: ASTNode) + case reset(targets: [String]) + + public var type: String { + switch self { + case .run: return "run" + case .continueConversation: return "continue_conversation" + case .openUrl: return "open_url" + case .set: return "set" + case .reset: return "reset" + } + } +} + +/// The ordered steps an `Action([...])` expression evaluates to. +public struct ActionPlan: Sendable, Equatable { + public var steps: [ActionStep] + + public init(steps: [ActionStep]) { + self.steps = steps + } +} + +/// A structured action event fired by interactive components. +public struct ActionEvent: Sendable, Equatable { + /// See `BuiltinActionType`; custom types pass through unchanged. + public var type: String + public var params: OpenUIObject + /// Human-readable label for the action (shown as the user message in chat). + public var humanFriendlyMessage: String + /// Form state at the time of the action. + public var formState: OpenUIObject? + public var formName: String? + + public init( + type: String, params: OpenUIObject = OpenUIObject(), humanFriendlyMessage: String, + formState: OpenUIObject? = nil, formName: String? = nil + ) { + self.type = type + self.params = params + self.humanFriendlyMessage = humanFriendlyMessage + self.formState = formState + self.formName = formName + } +} + +/// A two-way binding between a reactive prop and a `$state` variable. Setting +/// the field evaluates `expr` with `$value` bound to the new value. +public struct ReactiveAssign: Sendable, Equatable { + public var target: String + public var expr: ASTNode + + public init(target: String, expr: ASTNode) { + self.target = target + self.expr = expr + } +} + +/// A `Query(...)` statement with its positional arguments kept as AST. +public struct QueryStatementInfo: Sendable, Equatable { + public var statementId: String + public var toolAST: ASTNode? + public var argsAST: ASTNode? + public var defaultsAST: ASTNode? + public var refreshAST: ASTNode? + /// `$variable` dependencies of the arguments, collected at parse time. + public var deps: [String]? + public var complete: Bool +} + +/// A `Mutation(...)` statement with its positional arguments kept as AST. +public struct MutationStatementInfo: Sendable, Equatable { + public var statementId: String + public var toolAST: ASTNode? + public var argsAST: ASTNode? +} + +/// The result of parsing (or re-parsing while streaming) an OpenUI Lang response. +public struct ParseResult: Sendable, Equatable { + public struct Meta: Sendable, Equatable { + /// True if the input was truncated (still streaming). + public var incomplete: Bool + /// References used but not defined yet. + public var unresolved: [String] + /// Value statements defined but not reachable from the root. + public var orphaned: [String] + public var statementCount: Int + public var errors: [ValidationError] + } + + public var root: ElementNode? + public var meta: Meta + /// `$variable` declarations mapped to their default values. + public var stateDeclarations: OpenUIObject + public var queryStatements: [QueryStatementInfo] + public var mutationStatements: [MutationStatementInfo] + + static func empty(incomplete: Bool = true) -> ParseResult { + ParseResult( + root: nil, + meta: Meta( + incomplete: incomplete, unresolved: [], orphaned: [], statementCount: 0, errors: []), + stateDeclarations: OpenUIObject(), queryStatements: [], mutationStatements: []) + } +} + +/// A component parameter compiled from the library JSON Schema. +public struct ParamDef: Sendable, Equatable { + public var name: String + public var required: Bool + /// The schema `default`, used when a required prop is missing or null. + public var defaultValue: OpenUIValue? + public var schema: OpenUIValue? +} + +/// Positional parameter lists keyed by component name, in schema `$defs` order. +public struct ParamMap: Sendable, Equatable { + public var componentNames: [String] = [] + private var params: [String: [ParamDef]] = [:] + + public init() {} + + public subscript(component: String) -> [ParamDef]? { + get { params[component] } + set { + if params[component] == nil, newValue != nil { componentNames.append(component) } + params[component] = newValue + } + } +} diff --git a/packages/swift-lang/Sources/OpenUILang/Validation.swift b/packages/swift-lang/Sources/OpenUILang/Validation.swift new file mode 100644 index 0000000000..e96d72e022 --- /dev/null +++ b/packages/swift-lang/Sources/OpenUILang/Validation.swift @@ -0,0 +1,347 @@ +/// Mutable state threaded through one materialization pass. +final class MaterializeContext { + var syms: [String: ASTNode] + var cat: ParamMap? + var errors: [ValidationError] = [] + var unres: [String] = [] + var visited: Set = [] + var partial: Bool + /// The statement being materialized, for error attribution. + var currentStatementId: String? + /// Statements not reached yet, in insertion order. Whatever remains is orphaned. + var unreached: [String]? = nil + + init(syms: [String: ASTNode], cat: ParamMap?, partial: Bool, currentStatementId: String?) { + self.syms = syms + self.cat = cat + self.partial = partial + self.currentStatementId = currentStatementId + } + + func markReached(_ id: String) { + unreached?.removeAll { $0 == id } + } +} + +// MARK: - Schema introspection + +private let scalarTypeofs: Set = ["string", "number", "boolean"] + +/// `{ $ref: "#/$defs/Name" }` → `"Name"`. +private func refName(_ schema: OpenUIValue) -> String? { + guard let ref = schema["$ref"].stringValue else { return nil } + return ref.split(separator: "/", omittingEmptySubsequences: false).last.map(String.init) +} + +/// The `$ref` names a slot allows: one for `$ref`, several for anyOf/oneOf. +private func slotRefNames(_ schema: OpenUIObject) -> [String?] { + let options = schema["anyOf"]?.arrayValue ?? schema["oneOf"]?.arrayValue ?? [.object(schema)] + return options.map(refName) +} + +/// True when every option of the slot is a `$ref` to a component. +private func isOnlyComponentSlot(_ schema: OpenUIObject, _ ctx: MaterializeContext) -> Bool { + let names = slotRefNames(schema) + return !names.isEmpty && names.allSatisfy { name in name.map { ctx.cat?[$0] != nil } ?? false } +} + +private func isCompositeSchema(_ schema: OpenUIObject) -> Bool { + schema.contains("$ref") || schema.contains("anyOf") || schema.contains("oneOf") + || schema.contains("allOf") +} + +/// Scalar type or enum values of a JSON Schema leaf. +struct ScalarTypeInfo { + var expectedType: String? + var enumValues: [OpenUIValue]? +} + +func scalarTypeInfo(_ schema: OpenUIObject) -> ScalarTypeInfo { + if case .array(let values)? = schema["enum"] { return ScalarTypeInfo(enumValues: values) } + if let constant = schema["const"] { return ScalarTypeInfo(enumValues: [constant]) } + switch schema["type"] { + case .string("string"): return ScalarTypeInfo(expectedType: "string") + case .string("number"), .string("integer"): return ScalarTypeInfo(expectedType: "number") + case .string("boolean"): return ScalarTypeInfo(expectedType: "boolean") + default: return ScalarTypeInfo() + } +} + +/// A JSON Schema fragment's `default`; `nil` when it has none. +func schemaDefaultValue(_ property: OpenUIValue?) -> OpenUIValue? { + guard case .object(let object)? = property else { return nil } + return object["default"] +} + +/// The type hint used in signatures and messages, e.g. `string` or `"a"|"b"`. +func typeFromSchema(_ property: OpenUIValue?) -> String? { + guard case .object(let object)? = property else { return nil } + if case .string(let ref)? = object["$ref"] { + return ref.split(separator: "/", omittingEmptySubsequences: false).last.map(String.init) + } + let leaf = scalarTypeInfo(object) + if let enumValues = leaf.enumValues { + return enumValues.map { JSON.stringify($0) }.joined(separator: "|") + } + if let expectedType = leaf.expectedType { return expectedType } + if case .string(let type)? = object["type"] { return type } + return nil +} + +/// A typed signature like `Header(title*: string, variant: "a"|"b")`; `*` marks +/// required params, in positional order so an LLM can fix swapped arguments. +func buildParamsSignature(_ component: String, _ params: [ParamDef]) -> String { + let rendered = params.map { param -> String in + let marked = param.required ? "\(param.name)*" : param.name + if let type = typeFromSchema(param.schema) { return "\(marked): \(type)" } + return marked + } + return "\(component)(\(rendered.joined(separator: ", ")))" +} + +/// `Array.prototype.includes` (SameValueZero). +private func includes(_ values: [OpenUIValue], _ value: OpenUIValue) -> Bool { + values.contains { candidate in + if case .number(let a) = candidate, case .number(let b) = value { + return a == b || (a.isNaN && b.isNaN) + } + return candidate == value + } +} + +private func checkTypeMismatch(_ value: OpenUIValue, _ info: ScalarTypeInfo) -> ( + expected: String, actual: String +)? { + let actual = value.jsTypeof + if let enumValues = info.enumValues { + if !scalarTypeofs.contains(actual) { return nil } + if includes(enumValues, value) { return nil } + return ( + "one of [\(enumValues.map { JSON.stringify($0) }.joined(separator: ", "))]", + JSON.stringify(value) + ) + } + if let expectedType = info.expectedType, expectedType != actual { + return (expectedType, value.jsType) + } + return nil +} + +// MARK: - Errors + +enum ValidationIssue { + case typeMismatch(expected: String, actual: String) + case missingRequired(signature: String?) + case nullRequired(signature: String?) + case unknownComponent(available: [String]?) + case inlineReserved + case excessArgs(declared: Int, got: Int) + + var code: ValidationErrorCode { + switch self { + case .typeMismatch: return .typeMismatch + case .missingRequired: return .missingRequired + case .nullRequired: return .nullRequired + case .unknownComponent: return .unknownComponent + case .inlineReserved: return .inlineReserved + case .excessArgs: return .excessArgs + } + } +} + +private func validationMessage(_ component: String, _ path: String, _ issue: ValidationIssue) + -> String +{ + switch issue { + case .typeMismatch(let expected, let actual): + return "field \"\(path)\" expects \(expected) but got \(actual)" + case .missingRequired(let signature): + return "missing required field \"\(path)\"" + (signature.map { " — signature: \($0)" } ?? "") + case .nullRequired(let signature): + return "required field \"\(path)\" cannot be null" + + (signature.map { " — signature: \($0)" } ?? "") + case .unknownComponent(let available): + let list = + available.flatMap { + $0.isEmpty ? nil : ". Available components: \($0.joined(separator: ", "))" + } + ?? "" + return "Unknown component \"\(component)\" — not found in catalog or builtins\(list)" + case .inlineReserved: + return "\(component)() must be declared as a top-level statement, not used inline as a value" + case .excessArgs(let declared, let got): + return "\(component) takes \(declared) arg(s), got \(got) (\(got - declared) excess dropped)" + } +} + +func pushValidationIssue( + _ ctx: MaterializeContext, _ component: String, _ path: String, _ issue: ValidationIssue +) { + ctx.errors.append( + ValidationError( + code: issue.code, component: component, path: path, + message: validationMessage(component, path, issue), statementId: ctx.currentStatementId)) +} + +// MARK: - Invalid-value resolution (the edge rule) + +/// Substitutes a default for an invalid value when one exists. Otherwise a +/// required key propagates invalidity upward (returns true) and an optional +/// key is deleted. +func resolveInvalidValue( + _ container: inout OpenUIObject, _ key: String, required: Bool, defaultValue: OpenUIValue? +) -> Bool { + if let defaultValue { + container[key] = defaultValue + return false + } + if required { return true } + container[key] = nil + return false +} + +// MARK: - Validators + +/// Component elements in data slots: slots that admit components (`$ref`, +/// `anyOf`, `oneOf`) are unchecked, plain-data slots can never hold one. +private func validateElementPosition( + _ element: ElementNode, _ schema: OpenUIObject, _ component: String, _ path: String, + _ ctx: MaterializeContext +) -> Bool { + if isCompositeSchema(schema) { return false } + let type = schema["type"]?.stringValue + if type != nil || schema["enum"]?.arrayValue != nil || schema.contains("const") { + pushValidationIssue( + ctx, component, path, + .typeMismatch( + expected: type ?? "a literal value", actual: "component \"\(element.typeName)\"")) + return true + } + return false +} + +private func validateObjectValue( + _ value: inout OpenUIValue, _ schema: OpenUIObject, _ component: String, _ path: String, + _ ctx: MaterializeContext +) -> Bool { + guard case .object(var object) = value else { + pushValidationIssue( + ctx, component, path, .typeMismatch(expected: "object", actual: value.jsType)) + return true + } + let props = schema["properties"]?.objectValue ?? OpenUIObject() + let required: [String] = (schema["required"]?.arrayValue ?? []).compactMap(\.stringValue) + var invalid = false + // Structural checks are streaming-sensitive, so they only run on complete input. + if !ctx.partial { + for key in required { + let present = object.contains(key) + if object[key]?.isNullish ?? true { + if let fallback = schemaDefaultValue(props[key]) { + object[key] = fallback + continue + } + pushValidationIssue( + ctx, component, "\(path)/\(key)", + present ? .nullRequired(signature: nil) : .missingRequired(signature: nil)) + invalid = true + } + } + } + for key in props.keys where object.contains(key) { + let sub = props[key] + guard var child = object[key] else { continue } + let childInvalid = validateSchemaValue(&child, sub, component, "\(path)/\(key)", ctx) + object[key] = child + if childInvalid, + resolveInvalidValue( + &object, key, required: required.contains(key), defaultValue: schemaDefaultValue(sub)) + { + invalid = true + } + } + value = .object(object) + return invalid +} + +/// Invalid items are pruned in place, after validating all of them so error +/// paths keep their original indices. +private func validateArrayValue( + _ value: inout OpenUIValue, _ schema: OpenUIObject, _ component: String, _ path: String, + _ ctx: MaterializeContext +) -> Bool { + guard case .array(var items) = value else { + pushValidationIssue( + ctx, component, path, .typeMismatch(expected: "array", actual: value.jsType)) + return true + } + guard case .object = schema["items"] else { return false } + let itemSchema = schema["items"] + var invalid: [Int] = [] + for index in items.indices { + if validateSchemaValue(&items[index], itemSchema, component, "\(path)/\(index)", ctx) { + invalid.append(index) + } + } + for index in invalid.reversed() { items.remove(at: index) } + value = .array(items) + return false +} + +/// Enum membership is deferred while streaming (a partial literal may still +/// complete to a valid member); scalar type checks always run. +private func validateLeafValue( + _ value: OpenUIValue, _ schema: OpenUIObject, _ component: String, _ path: String, + _ ctx: MaterializeContext +) -> Bool { + let leaf = scalarTypeInfo(schema) + if leaf.expectedType == nil && leaf.enumValues == nil { return false } + if leaf.enumValues != nil && ctx.partial { return false } + if let mismatch = checkTypeMismatch(value, leaf) { + pushValidationIssue( + ctx, component, path, .typeMismatch(expected: mismatch.expected, actual: mismatch.actual)) + return true + } + return false +} + +/// Validates a materialized value against a JSON Schema fragment, pruning +/// unsalvageable data along the way. Returns true when the value itself is +/// invalid; the caller then applies the edge rule (`resolveInvalidValue`). +func validateSchemaValue( + _ value: inout OpenUIValue, _ schema: OpenUIValue?, _ component: String, _ path: String, + _ ctx: MaterializeContext +) -> Bool { + guard case .object(let s)? = schema else { return false } + // Absence is handled by the parent's required checks. + if value.isNullish { return false } + switch value { + case .ast: + // Runtime expressions resolve later. + return false + case .element(let element): + return validateElementPosition(element, s, component, path, ctx) + default: + break + } + if isCompositeSchema(s) { + // Data (an object, array, string, number or boolean) in a component-only slot is invalid. + guard isOnlyComponentSlot(s, ctx) else { return false } + let actual: String + switch value { + case .array: actual = "array" + case .string: actual = "string" + case .number: actual = "number" + case .bool: actual = "boolean" + default: actual = "plain object" + } + let expected = slotRefNames(s).map { $0 ?? "" }.joined(separator: " | ") + pushValidationIssue(ctx, component, path, .typeMismatch(expected: expected, actual: actual)) + return true + } + switch s["type"] { + case .string("object"): return validateObjectValue(&value, s, component, path, ctx) + case .string("array"): return validateArrayValue(&value, s, component, path, ctx) + default: return validateLeafValue(value, s, component, path, ctx) + } +} diff --git a/packages/swift-lang/Sources/OpenUILang/Value.swift b/packages/swift-lang/Sources/OpenUILang/Value.swift new file mode 100644 index 0000000000..55c77e3300 --- /dev/null +++ b/packages/swift-lang/Sources/OpenUILang/Value.swift @@ -0,0 +1,148 @@ +/// A dynamically typed value flowing through the parser and runtime. +/// +/// lang-core passes `unknown` values around and relies on JavaScript semantics +/// for them, so this enum keeps the distinctions those semantics depend on: +/// `undefined` versus `null`, and the parser/runtime markers (element nodes, +/// preserved AST expressions, action plans, reactive assignments) that live +/// inside props next to plain data. +public indirect enum OpenUIValue: Sendable, Equatable { + case undefined + case null + case bool(Bool) + case number(Double) + case string(String) + case array([OpenUIValue]) + case object(OpenUIObject) + case element(ElementNode) + case ast(ASTNode) + case actionPlan(ActionPlan) + case actionStep(ActionStep) + case reactiveAssign(ReactiveAssign) + + /// `null` or `undefined` (JavaScript's `value == null`). + public var isNullish: Bool { + switch self { + case .undefined, .null: return true + default: return false + } + } + + public var stringValue: String? { + if case .string(let value) = self { return value } + return nil + } + + public var numberValue: Double? { + if case .number(let value) = self { return value } + return nil + } + + public var boolValue: Bool? { + if case .bool(let value) = self { return value } + return nil + } + + public var arrayValue: [OpenUIValue]? { + if case .array(let value) = self { return value } + return nil + } + + public var objectValue: OpenUIObject? { + if case .object(let value) = self { return value } + return nil + } + + public var elementValue: ElementNode? { + if case .element(let value) = self { return value } + return nil + } + + /// Member lookup for plain objects; `undefined` for anything else. + public subscript(key: String) -> OpenUIValue { + if case .object(let object) = self { return object[key] ?? .undefined } + return .undefined + } +} + +extension OpenUIValue: ExpressibleByStringLiteral, ExpressibleByBooleanLiteral, + ExpressibleByFloatLiteral, ExpressibleByIntegerLiteral, ExpressibleByNilLiteral, + ExpressibleByArrayLiteral, ExpressibleByDictionaryLiteral +{ + public init(stringLiteral value: String) { self = .string(value) } + public init(booleanLiteral value: Bool) { self = .bool(value) } + public init(floatLiteral value: Double) { self = .number(value) } + public init(integerLiteral value: Int) { self = .number(Double(value)) } + public init(nilLiteral: ()) { self = .null } + public init(arrayLiteral elements: OpenUIValue...) { self = .array(elements) } + public init(dictionaryLiteral elements: (String, OpenUIValue)...) { + var object = OpenUIObject() + for (key, value) in elements { object[key] = value } + self = .object(object) + } +} + +/// An object with JavaScript property ordering. +/// +/// JavaScript enumerates integer-like keys first in ascending order, then the +/// remaining keys in insertion order. Prop order drives positional argument +/// mapping and error order, so the port keeps the same rule instead of using an +/// unordered dictionary. +public struct OpenUIObject: Sendable, Equatable, Sequence { + private var insertionOrder: [String] = [] + private var storage: [String: OpenUIValue] = [:] + + public init() {} + + public init(_ entries: [(String, OpenUIValue)]) { + for (key, value) in entries { self[key] = value } + } + + public var count: Int { storage.count } + public var isEmpty: Bool { storage.isEmpty } + + /// Keys in JavaScript enumeration order. + public var keys: [String] { + let indexKeys = insertionOrder.compactMap { key in arrayIndex(key).map { (key, $0) } } + guard !indexKeys.isEmpty else { return insertionOrder } + let sortedIndexKeys = indexKeys.sorted { $0.1 < $1.1 }.map(\.0) + return sortedIndexKeys + insertionOrder.filter { arrayIndex($0) == nil } + } + + public var values: [OpenUIValue] { keys.map { storage[$0]! } } + + public var entries: [(key: String, value: OpenUIValue)] { keys.map { ($0, storage[$0]!) } } + + public func contains(_ key: String) -> Bool { storage[key] != nil } + + /// Assigning `nil` deletes the key (JavaScript `delete`). + public subscript(key: String) -> OpenUIValue? { + get { storage[key] } + set { + if let newValue { + if storage.updateValue(newValue, forKey: key) == nil { insertionOrder.append(key) } + } else if storage.removeValue(forKey: key) != nil { + insertionOrder.removeAll { $0 == key } + } + } + } + + public func makeIterator() -> IndexingIterator<[(key: String, value: OpenUIValue)]> { + entries.makeIterator() + } + + public static func == (lhs: OpenUIObject, rhs: OpenUIObject) -> Bool { + lhs.keys == rhs.keys && lhs.storage == rhs.storage + } +} + +/// A canonical array index (`"0"` … `"4294967294"`), which JavaScript orders first. +private func arrayIndex(_ key: String) -> UInt32? { + guard let value = UInt32(key), value != UInt32.max, String(value) == key else { return nil } + return value +} + +extension OpenUIObject: ExpressibleByDictionaryLiteral { + public init(dictionaryLiteral elements: (String, OpenUIValue)...) { + self.init(elements) + } +} diff --git a/packages/swift-lang/Sources/OpenUISwiftUI/ComponentProps.swift b/packages/swift-lang/Sources/OpenUISwiftUI/ComponentProps.swift new file mode 100644 index 0000000000..c586c9e87e --- /dev/null +++ b/packages/swift-lang/Sources/OpenUISwiftUI/ComponentProps.swift @@ -0,0 +1,61 @@ +import OpenUILang +import SwiftUI + +/// A component's evaluated props, with typed accessors that tolerate the +/// missing and partial values a streaming response produces. +public struct ComponentProps { + public let typeName: String + public let statementId: String? + public let values: OpenUIObject + + public init(_ element: ElementNode) { + typeName = element.typeName + statementId = element.statementId + values = element.props + } + + public subscript(key: String) -> OpenUIValue { values[key] ?? .undefined } + + /// The prop as a string, if it is one. + public func string(_ key: String) -> String? { self[key].stringValue } + + /// The prop as display text: strings as-is, numbers and booleans formatted + /// like JavaScript, anything else empty. + public func text(_ key: String) -> String { displayText(self[key]) } + + public func number(_ key: String) -> Double? { self[key].numberValue } + + public func bool(_ key: String) -> Bool? { self[key].boolValue } + + public func array(_ key: String) -> [OpenUIValue] { self[key].arrayValue ?? [] } + + /// Child elements in the prop (arrays skip non-element items). + public func elements(_ key: String) -> [ElementNode] { + switch self[key] { + case .element(let element): return [element] + case .array(let items): return items.compactMap(\.elementValue) + default: return [] + } + } + + /// Child element props, for components that read sub-components' data + /// (e.g. a chart reading its `Series`). + public func children(_ key: String) -> [ComponentProps] { + elements(key).map(ComponentProps.init) + } +} + +/// Text for scalar values: strings as-is, numbers and booleans like JavaScript. +public func displayText(_ value: OpenUIValue) -> String { + switch value { + case .string(let string): return string + case .number, .bool: return value.jsString + default: return "" + } +} + +extension Double { + /// The number, or nil when it's infinite or NaN. Numbers in a response can + /// be either (1e999, 0/0), and as sizes or ranges they'd break the layout. + var finite: Double? { isFinite ? self : nil } +} diff --git a/packages/swift-lang/Sources/OpenUISwiftUI/Components/CardComponents.swift b/packages/swift-lang/Sources/OpenUISwiftUI/Components/CardComponents.swift new file mode 100644 index 0000000000..4778ca0c2d --- /dev/null +++ b/packages/swift-lang/Sources/OpenUISwiftUI/Components/CardComponents.swift @@ -0,0 +1,809 @@ +import OpenUILang +import SwiftUI + +// MARK: - Click context + +/// Merges an item's click context into a card block's `action`, like +/// react-ui's `withItemContext`: +/// - no action: continue the conversation with the item context as params; +/// - an `Action([...])` plan: append "Selected item: {…}" to its +/// `@ToAssistant` contexts (the only channel the host receives for them); +/// - a legacy `{ type, params, url, context }` object: merge into params. +func withItemContext(_ action: OpenUIValue, _ itemContext: OpenUIObject) -> OpenUIValue { + let context = OpenUIObject(itemContext.entries.filter { $0.value != .undefined }) + switch action { + case .undefined, .null: + return ["type": .string(BuiltinActionType.continueConversation), "params": .object(context)] + case .actionPlan(let plan): + if context.isEmpty { return action } + let suffix = "Selected item: \(JSON.stringify(.object(context)))" + return .actionPlan( + ActionPlan( + steps: plan.steps.map { step in + guard case .continueConversation(let message, let existing) = step else { return step } + let merged = existing.map { $0.isEmpty ? suffix : "\($0)\n\(suffix)" } ?? suffix + return .continueConversation(message: message, context: merged) + })) + default: + var params = action["params"].objectValue ?? OpenUIObject() + if action["url"] != .undefined { params["url"] = action["url"] } + if action["context"].isTruthy { params["context"] = action["context"] } + for (key, value) in context { params[key] = value } + let type = + action["type"].isNullish ? .string(BuiltinActionType.continueConversation) : action["type"] + return ["type": type, "params": .object(params)] + } +} + +/// A string prop of a child element (`getStringProp` in react-ui). +private func childString(_ node: OpenUIValue, _ key: String) -> String? { + node.elementValue?.props[key]?.stringValue +} + +private struct TextAlignmentKey: EnvironmentKey { + static let defaultValue = TextAlignment.leading +} + +extension EnvironmentValues { + /// How text building blocks align, e.g. trailing in a snippet card's value column. + fileprivate var openUITextAlignment: TextAlignment { + get { self[TextAlignmentKey.self] } + set { self[TextAlignmentKey.self] = newValue } + } +} + +// MARK: - Blocks + +/// Card layout sizes from react-ui: small blocks use 280pt carousel cards, +/// medium ones 320pt (280pt in narrow blocks). +private enum CardBlockSize { + case small, medium +} + +/// react-ui's `getRowConfiguration`: how many cards go in each grid row. +func cardRowConfiguration(_ count: Int, maxPerRow: Int) -> [Int] { + if count <= 0 { return [] } + if count == 1 { return [1] } + if maxPerRow == 2 { + return Array(repeating: 2, count: count / 2) + (count % 2 == 1 ? [1] : []) + } + if count % 3 == 0 { return Array(repeating: 3, count: count / 3) } + if count % 3 == 2 { + var rows = Array(repeating: 3, count: count / 3) + rows.insert(2, at: (rows.count + 1) / 2) + return rows + } + return Array(repeating: 3, count: (count - 4) / 3) + [2, 2] +} + +/// Rows of up to `maxPerRow` cells, two columns in grids 768pt wide or less +/// (an odd last cell spans both) and one column at 480pt or less, like +/// react-ui's responsive card grids. Cells in a row share the tallest height. +struct ResponsiveCardGrid: View { + let count: Int + let maxPerRow: Int + var responsive = true + var spacing: CGFloat = 12 + var minCellWidth: CGFloat = 0 + @ViewBuilder let cell: (Int) -> Cell + + var body: some View { + CardGridLayout( + maxPerRow: maxPerRow, responsive: responsive, spacing: spacing, minCellWidth: minCellWidth + ) { + ForEach(0.. CGSize { + guard let width = proposal.width, width.isFinite else { + // No width to fit (e.g. measuring an ideal size): one column. + let sizes = subviews.map { $0.sizeThatFits(.unspecified) } + let height = sizes.map(\.height).reduce(0, +) + spacing * CGFloat(max(sizes.count - 1, 0)) + return CGSize(width: sizes.map(\.width).max() ?? 0, height: height) + } + let height = rows(subviews.count, width).reduce(into: CGFloat(0)) { total, row in + total += rowHeight(row, subviews, width) + } + return CGSize( + width: width, height: height + spacing * CGFloat(max(rowCount(subviews.count, width) - 1, 0))) + } + + func placeSubviews( + in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout () + ) { + var y = bounds.minY + for row in rows(subviews.count, bounds.width) { + let width = cellWidth(row.count, bounds.width) + let height = rowHeight(row, subviews, bounds.width) + for (column, index) in row.enumerated() { + subviews[index].place( + at: CGPoint(x: bounds.minX + CGFloat(column) * (width + spacing), y: y), + proposal: ProposedViewSize(width: width, height: height)) + } + y += height + spacing + } + } + + private func cellWidth(_ columns: Int, _ width: CGFloat) -> CGFloat { + (width - spacing * CGFloat(columns - 1)) / CGFloat(columns) + } + + private func rowHeight(_ row: [Int], _ subviews: Subviews, _ width: CGFloat) -> CGFloat { + let proposal = ProposedViewSize(width: cellWidth(row.count, width), height: nil) + return row.map { subviews[$0].sizeThatFits(proposal).height }.max() ?? 0 + } + + private func rowCount(_ count: Int, _ width: CGFloat) -> Int { rows(count, width).count } + + /// The most cards per row: react-ui's breakpoints, then as many as fit at + /// `minCellWidth`. + static func columns( + width: CGFloat, maxPerRow: Int, responsive: Bool, spacing: CGFloat, minCellWidth: CGFloat + ) -> Int { + let breakpoint = !responsive ? maxPerRow : width <= 480 ? 1 : width <= 768 ? 2 : maxPerRow + guard minCellWidth > 0 else { return breakpoint } + let fitting = Int((width + spacing) / (minCellWidth + spacing)) + return max(1, min(breakpoint, fitting)) + } + + /// Subview indices per row, using react-ui's breakpoints. + private func rows(_ count: Int, _ width: CGFloat) -> [[Int]] { + let columns = Self.columns( + width: width, maxPerRow: maxPerRow, responsive: responsive, spacing: spacing, + minCellWidth: minCellWidth) + let perRow = + columns == 1 + ? Array(repeating: 1, count: count) : cardRowConfiguration(count, maxPerRow: columns) + var start = 0 + return perRow.map { length in + defer { start += length } + return Array(start..: View { + let props: ComponentProps + let size: CardBlockSize + let maxPerRow: Int + /// The card's minimum width in react-ui's styles, if it has one. + var minCardWidth: CGFloat = 0 + let click: (Int, ComponentProps) -> (label: String, context: OpenUIObject) + @ViewBuilder let item: (ComponentProps) -> Item + @Environment(OpenUIContext.self) private var context + @Environment(\.openUIFormName) private var form + + var body: some View { + let items = props.children("items") + let gap = props.number("gap").flatMap(\.finite).map { CGFloat(max($0, 0)) } ?? 12 + let clickable = props["action"].isTruthy && !context.isStreaming + Group { + if props.string("layout") == "carousel" { + CarouselScroller(count: items.count, spacing: gap, fade: 48, verticalPadding: 4) { index in + card(index, items[index], clickable: clickable) + // Sized from the visible width in the same layout pass: measuring + // it into state instead lays the cards out at the wrong width for a + // frame, then changes their height. + .containerRelativeFrame(.horizontal) { width, _ in + size == .small || width <= 480 ? 280 : 320 + } + } + } else { + ResponsiveCardGrid( + count: items.count, maxPerRow: maxPerRow, responsive: props.bool("responsive") != false, + spacing: gap, minCellWidth: minCardWidth + ) { index in + card(index, items[index], clickable: clickable) + } + } + } + .frame(maxWidth: .infinity, alignment: .leading) + } + + @ViewBuilder + private func card(_ index: Int, _ entry: ComponentProps, clickable: Bool) -> some View { + let content = item(entry).environment(\.openUICardClickable, clickable) + if clickable { + Button { + let (label, itemContext) = click(index, entry) + context.triggerAction( + label, form: form, action: withItemContext(props["action"], itemContext)) + } label: { + content.contentShape(Rectangle()) + } + .buttonStyle(.openUICard) + } else { + content + } + } +} + +private struct CardClickableKey: EnvironmentKey { + static let defaultValue = false +} + +extension EnvironmentValues { + /// Whether the card being drawn is clickable (its block has an action). + fileprivate var openUICardClickable: Bool { + get { self[CardClickableKey.self] } + set { self[CardClickableKey.self] = newValue } + } +} + +/// The chevron clickable cards show in a corner. +private struct CardChevron: View { + var size: CGFloat = 14 + var body: some View { + Image(systemName: "chevron.right").font(.system(size: size * 0.8, weight: .semibold)) + } +} + +/// The small-card surface: faint fill when static, raised with an +/// interactive border and a light shadow when clickable. +private struct SmallCardSurface: ViewModifier { + let clickable: Bool + @Environment(\.openUITheme) private var theme + + func body(content: Content) -> some View { + content + .padding(10) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + .background( + clickable ? theme.surface : theme.subtleSurface, in: RoundedRectangle(cornerRadius: 14) + ) + .overlay( + RoundedRectangle(cornerRadius: 14).strokeBorder( + clickable ? theme.interactiveBorder : theme.border) + ) + .shadow(color: .black.opacity(clickable ? 0.05 : 0), radius: 2, y: 1) + } +} + +struct SnippetCardBlockView: View { + let props: ComponentProps + var body: some View { + CardBlockLayout(props: props, size: .small, maxPerRow: 2) { index, item in + let title = childString(item["lhs"], "title") + return ( + title ?? item.string("id") ?? "Snippet card \(index + 1)", + [ + "itemIndex": .number(Double(index)), "itemId": item["id"], + "itemTitle": title.map(OpenUIValue.string) ?? .undefined, + "itemSubtitle": childString(item["lhs"], "subtitle").map(OpenUIValue.string) + ?? .undefined, + "itemValue": childString(item["rhs"], "value").map(OpenUIValue.string) ?? .undefined, + ] + ) + } item: { + SnippetCard(item: $0) + } + } +} + +struct SnippetCardItemView: View { + let props: ComponentProps + var body: some View { SnippetCard(item: props) } +} + +/// One row: the label on the left, the value (or a chevron) on the right. +private struct SnippetCard: View { + let item: ComponentProps + @Environment(\.openUICardClickable) private var clickable + + var body: some View { + HStack(alignment: .center, spacing: 12) { + OpenUINode(item["lhs"]).frame(maxWidth: .infinity, alignment: .leading) + if !item["rhs"].isNullish { + OpenUINode(item["rhs"]) + .environment(\.openUITextAlignment, .trailing) + .fixedSize() + } else if clickable { + CardChevron().foregroundStyle(.secondary) + } + } + .modifier(SmallCardSurface(clickable: clickable)) + } +} + +struct OverviewCardBlockView: View { + let props: ComponentProps + var body: some View { + CardBlockLayout(props: props, size: .small, maxPerRow: 3, minCardWidth: 158) { index, item in + let title = childString(item["top"], "title") ?? childString(item["top"], "value") + let subtitle = childString(item["top"], "subtitle") ?? childString(item["top"], "subtext") + return ( + title ?? item.string("id") ?? "Overview card \(index + 1)", + [ + "itemIndex": .number(Double(index)), "itemId": item["id"], + "itemTitle": title.map(OpenUIValue.string) ?? .undefined, + "itemSubtitle": subtitle.map(OpenUIValue.string) ?? .undefined, + "itemMetricValue": item["bottom"].elementValue?.props["value"] ?? .undefined, + ] + ) + } item: { + OverviewCard(item: $0) + } + } +} + +struct OverviewCardItemView: View { + let props: ComponentProps + var body: some View { OverviewCard(item: props) } +} + +/// A top slot (with the chevron) and a bottom metric, spread vertically. +private struct OverviewCard: View { + let item: ComponentProps + @Environment(\.openUICardClickable) private var clickable + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + HStack(alignment: .top, spacing: 8) { + OpenUINode(item["top"]).frame(maxWidth: .infinity, alignment: .leading) + if clickable { CardChevron().foregroundStyle(.secondary) } + } + .frame(minHeight: 24, alignment: .top) + if !item["bottom"].isNullish { + Spacer(minLength: 18) + OpenUINode(item["bottom"]) + } + } + .modifier(SmallCardSurface(clickable: clickable)) + } +} + +struct ContextCardBlockView: View { + let props: ComponentProps + var body: some View { + CardBlockLayout(props: props, size: .small, maxPerRow: 3, minCardWidth: 196) { index, item in + let title = contextTitle(item) + return ( + title.isEmpty ? (item.string("id") ?? "Context card \(index + 1)") : title, + [ + "itemIndex": .number(Double(index)), "itemId": item["id"], "itemTitle": .string(title), + "itemBody": item["body"], "itemBgColor": item["bgColor"], + "itemBgImageSrc": item["bgImageSrc"], "itemBgImageAlt": item["bgImageAlt"], + ] + ) + } item: { + ContextCard(item: $0) + } + } +} + +struct ContextCardItemView: View { + let props: ComponentProps + var body: some View { ContextCard(item: props) } +} + +/// A context card's title text: the string, or its Tag's text. +private func contextTitle(_ item: ComponentProps) -> String { + switch item["title"] { + case .string(let title): return title + case .element(let tag): return tag.props["text"]?.stringValue ?? "" + default: return "" + } +} + +/// A small title (or Tag) at the top and bold body text at the bottom, on a +/// tinted fill or a darkened photo. +private struct ContextCard: View { + let item: ComponentProps + @Environment(\.openUICardClickable) private var clickable + @Environment(\.openUITheme) private var theme + + var body: some View { + let image = item.string("bgImageSrc").flatMap { $0.isEmpty ? nil : $0 } + VStack(alignment: .leading, spacing: 0) { + HStack(alignment: .top, spacing: 8) { + Group { + if case .element = item["title"] { + OpenUINode(item["title"]).environment(\.openUITagOnImage, image != nil) + } else { + Text(item.text("title")).font(.subheadline) + .foregroundStyle( + image == nil ? AnyShapeStyle(.secondary) : AnyShapeStyle(.white.opacity(0.8))) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + if clickable { CardChevron(size: 16) } + } + .frame(minHeight: 24, alignment: .top) + Spacer(minLength: 18) + if let body = item.string("body"), !body.isEmpty { + Text(body).font(.body.weight(.semibold)) + } + } + .padding(12) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + .foregroundStyle(image == nil ? Color.primary : Color.white) + .background { + if let image { + RemoteImage(src: image, alt: item.text("bgImageAlt")) + .overlay( + LinearGradient( + colors: [.black.opacity(0.25), .black.opacity(0.65)], startPoint: .top, + endPoint: .bottom)) + } else if clickable || item.string("bgColor") == "gray" { + theme.sunkSurface + } else { + theme.subtleSurface + } + } + .clipShape(RoundedRectangle(cornerRadius: 12)) + } +} + +struct CompositeCardBlockView: View { + let props: ComponentProps + var body: some View { + CardBlockLayout(props: props, size: .medium, maxPerRow: 2, minCardWidth: 240) { index, item in + let header = item["header"].elementValue?.props ?? OpenUIObject() + func pick(_ keys: String...) -> String? { + keys.lazy.compactMap { header[$0]?.stringValue }.first + } + let title = pick("title", "value") + let alt = pick("alt") + let footer = item["footer"] + let label = [title, alt, item.string("id")].compactMap { $0 }.first { !$0.isEmpty } + return ( + label ?? "Composite card \(index + 1)", + [ + "itemIndex": .number(Double(index)), "itemId": item["id"], + "itemHeaderTitle": title.map(OpenUIValue.string) ?? .undefined, + "itemHeaderSubtitle": pick("subtitle", "subtext").map(OpenUIValue.string) ?? .undefined, + "itemHeaderAlt": alt.map(OpenUIValue.string) ?? .undefined, + "itemBodyCount": .number(Double(item.array("body").count)), + "itemFooterPrice": footer["price"].elementValue?.props["value"] ?? .undefined, + "itemFooterButtonLabel": footer["button"].elementValue?.props["label"] ?? .undefined, + ] + ) + } item: { + CompositeCard(item: $0) + } + } +} + +struct CompositeCardItemView: View { + let props: ComponentProps + var body: some View { CompositeCard(item: props) } +} + +/// Header, stacked body content, and a price-over-button footer. +private struct CompositeCard: View { + let item: ComponentProps + @Environment(\.openUICardClickable) private var clickable + @Environment(\.openUITheme) private var theme + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + OpenUINode(item["header"]) + OpenUINodes(item.array("body"), spacing: 12) + let footer = item["footer"] + if !footer["price"].isNullish || !footer["button"].isNullish { + VStack(alignment: .leading, spacing: 8) { + OpenUINode(footer["price"]) + OpenUINode(footer["button"]).environment(\.openUIButtonFillsWidth, true) + } + } + } + .padding(12) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + .background( + clickable ? theme.surface : theme.subtleSurface, in: RoundedRectangle(cornerRadius: 16) + ) + .overlay( + RoundedRectangle(cornerRadius: 16).strokeBorder( + clickable ? theme.interactiveBorder : theme.border)) + } +} + +struct VisualCardBlockView: View { + let props: ComponentProps + var body: some View { + CardBlockLayout(props: props, size: .medium, maxPerRow: 3) { index, item in + let bodyValue = item["body"].elementValue?.props["value"]?.stringValue + let tagText = item["tag"].elementValue?.props["text"]?.stringValue + let label = [bodyValue, tagText, item.string("id")].compactMap { $0 }.first { !$0.isEmpty } + return ( + label ?? "Visual card \(index + 1)", + [ + "itemIndex": .number(Double(index)), "itemId": item["id"], + "itemTag": tagText.map(OpenUIValue.string) ?? .undefined, + "itemBody": bodyValue.map(OpenUIValue.string) ?? .undefined, + "itemBodySubtext": item["body"].elementValue?.props["subtext"] ?? .undefined, + "itemBgImageSrc": item["bgImageSrc"], "itemBgImageAlt": item["bgImageAlt"], + ] + ) + } item: { + VisualCard(item: $0) + } + } +} + +struct VisualCardItemView: View { + let props: ComponentProps + var body: some View { VisualCard(item: props) } +} + +/// A photo-first card: the image fills the card, the tag and chevron sit on +/// top, and the text panel floats over the bottom of the photo. +private struct VisualCard: View { + let item: ComponentProps + @Environment(\.openUICardClickable) private var clickable + @Environment(\.openUIHovered) private var hovered + @Environment(\.openUITheme) private var theme + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + HStack(alignment: .top) { + OpenUINode(item["tag"]).environment(\.openUITagOnImage, true) + Spacer(minLength: 8) + if clickable { + CardChevron(size: 16) + .foregroundStyle(.white) + .padding(4) + .background(.black.opacity(0.4), in: RoundedRectangle(cornerRadius: 6)) + } + } + .frame(minHeight: 24, alignment: .top) + .padding(12) + Spacer(minLength: 0) + if textBlockHasContent(item["body"]) { + OpenUINode(item["body"]) + .padding(.vertical, 8) + .padding(.horizontal, 10) + .frame(maxWidth: .infinity, alignment: .leading) + .background(theme.surface, in: RoundedRectangle(cornerRadius: 10)) + .overlay(RoundedRectangle(cornerRadius: 10).strokeBorder(theme.border)) + .padding([.horizontal, .bottom], 12) + } + } + .frame(maxWidth: .infinity, minHeight: 280, maxHeight: .infinity, alignment: .topLeading) + .background { + ZStack { + theme.subtleSurface + RemoteImage(src: item.string("bgImageSrc"), alt: item.text("bgImageAlt")) + // react-ui zooms a clickable card's photo under the pointer. + .scaleEffect(hovered ? 1.04 : 1) + .animation(.easeOut(duration: 0.3), value: hovered) + } + } + .clipShape(RoundedRectangle(cornerRadius: 16)) + .overlay(RoundedRectangle(cornerRadius: 16).strokeBorder(theme.border)) + } +} + +// MARK: - Building blocks + +/// Whether a card's text block (`Text`, `BoldText`) has anything to show. A +/// response can bind one to values that come out empty (a loop variable used +/// outside its `@Each`), and its panel would then float empty over the photo. +func textBlockHasContent(_ value: OpenUIValue) -> Bool { + guard let element = value.elementValue, ["Text", "BoldText"].contains(element.typeName) else { + return !value.isNullish + } + return ["value", "subtext"].contains { !displayText(element.props[$0] ?? .undefined).isEmpty } +} + +/// `Text` / `BoldText`: a value line with optional subtext. `variant "number"` +/// uses tabular digits; `subtextVariant "metric"` colors a leading +/- green/red. +struct TextLineView: View { + let props: ComponentProps + let bold: Bool + @Environment(\.openUITextAlignment) private var alignment + @Environment(\.openUITheme) private var theme + + var body: some View { + VStack(alignment: alignment == .trailing ? .trailing : .leading, spacing: 2) { + Text(props.text("value")) + .font(font.weight(bold ? .semibold : .regular)) + .monospacedDigit(props.string("variant") == "number") + if let subtext = props.string("subtext"), !subtext.isEmpty { + Text(subtext) + .font(.caption) + .monospacedDigit(props.string("subtextVariant") != "text") + .foregroundStyle(subtextColor(subtext)) + } + } + } + + private var font: Font { + switch props.string("size") { + case "xs": return .footnote + case "sm": return .subheadline + case "lg": return .title2 + default: return .body + } + } + + private func subtextColor(_ subtext: String) -> Color { + guard props.string("subtextVariant") == "metric" else { return .secondary } + if subtext.hasPrefix("+") { return theme.success } + if subtext.hasPrefix("-") || subtext.hasPrefix("−") { return theme.danger } + return .secondary + } +} + +extension View { + @ViewBuilder + fileprivate func monospacedDigit(_ enabled: Bool) -> some View { + if enabled { monospacedDigit() } else { self } + } +} + +struct IconTextView: View { + let props: ComponentProps + + var body: some View { + let vertical = props.string("layout") == "vertical" + let layout = + vertical + ? AnyLayout(VStackLayout(alignment: .leading, spacing: 8)) + : AnyLayout(HStackLayout(spacing: 10)) + layout { + IconBadge( + symbol: iconSymbol(props["icon"]) + ?? LucideSymbols.systemName(for: "circle-dot", category: nil), + variant: props.string("iconVariant"), size: props.string("iconSize")) + TitleStack(props: props) + } + } +} + +/// An icon on a tinted rounded square. +private struct IconBadge: View { + let symbol: String + let variant: String? + let size: String? + @Environment(\.openUITheme) private var theme + + var body: some View { + let side: CGFloat = + switch size { + case "xs": 20 + case "s", "sm": 26 + case "l", "lg": 40 + case "xl": 48 + default: 32 + } + let (foreground, background) = colors + Image(systemName: symbol) + .font(.system(size: side * 0.5)) + .foregroundStyle(foreground) + .frame(width: side, height: side) + .background(background, in: RoundedRectangle(cornerRadius: theme.smallCornerRadius)) + } + + private var colors: (Color, Color) { + switch variant { + case "info", "success", "warning", "danger": + let color = theme.status(variant) + return (color, color.opacity(0.14)) + case "inverted": return (Color(white: 0.98), Color.primary) + case "filled": return (theme.onAccent, theme.accent) + case "soft": return (theme.accent, theme.accent.opacity(0.14)) + default: return (.primary, theme.sunkSurface) + } + } +} + +/// Title (bold when asked) with an optional subtitle. +private struct TitleStack: View { + let props: ComponentProps + var body: some View { + VStack(alignment: .leading, spacing: 2) { + Text(props.text("title")).fontWeight(props.bool("bold") == true ? .semibold : .medium) + if let subtitle = props.string("subtitle"), !subtitle.isEmpty { + Text(subtitle).font(.caption).foregroundStyle(.secondary) + } + } + } +} + +struct ImageTextView: View { + let props: ComponentProps + + var body: some View { + let size = CGFloat(min(max(props.number("imageSize").flatMap(\.finite) ?? 40, 0), 400)) + let vertical = props.string("layout") == "vertical" + let layout = + vertical + ? AnyLayout(VStackLayout(alignment: .leading, spacing: 8)) + : AnyLayout(HStackLayout(spacing: 10)) + layout { + RemoteImage(src: props.string("src"), alt: props.text("alt")) + .frame(width: size, height: size) + TitleStack(props: props) + } + } +} + +struct ImageTextLargeView: View { + let props: ComponentProps + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + RemoteImage(src: props.string("src"), alt: props.text("alt")) + .frame(height: 160) + .clipped() + TitleStack(props: props) + } + } +} + +struct MetricIndicatorInlineView: View { + let props: ComponentProps + + var body: some View { + VStack(alignment: .leading, spacing: 2) { + HStack(alignment: .firstTextBaseline, spacing: 6) { + Text(props.text("value")).font(.headline.monospacedDigit()) + TrendLabel(trend: props["trend"]) + } + if let subtext = props.string("subtext"), !subtext.isEmpty { + Text(subtext).font(.caption).foregroundStyle(.secondary) + } + } + } +} + +struct MetricIndicatorWithStrikethroughView: View { + let props: ComponentProps + + var body: some View { + VStack(alignment: .leading, spacing: 2) { + HStack(alignment: .firstTextBaseline, spacing: 6) { + Text(props.text("value")).font(.title3.weight(.semibold).monospacedDigit()) + if let previous = props.string("previousValue"), !previous.isEmpty { + Text(previous).font(.subheadline).strikethrough().foregroundStyle(.secondary) + } + TrendLabel(trend: props["trend"]) + } + if let subtext = props.string("subtext"), !subtext.isEmpty { + Text(subtext).font(.caption).foregroundStyle(.secondary) + } + } + } +} + +/// `{ direction, value }` as an arrow and percentage, in the success color +/// going up and the danger color going down. +private struct TrendLabel: View { + let trend: OpenUIValue + @Environment(\.openUITheme) private var theme + + var body: some View { + if let value = trend["value"].numberValue { + let up = trend["direction"].stringValue == "up" + HStack(spacing: 2) { + Image(systemName: up ? "arrow.up.right" : "arrow.down.right") + Text("\(jsNumberToString(value))%") + } + .font(.caption.weight(.medium).monospacedDigit()) + .foregroundStyle(up ? theme.success : theme.danger) + } + } +} diff --git a/packages/swift-lang/Sources/OpenUISwiftUI/Components/ChartComponents.swift b/packages/swift-lang/Sources/OpenUISwiftUI/Components/ChartComponents.swift new file mode 100644 index 0000000000..053733f580 --- /dev/null +++ b/packages/swift-lang/Sources/OpenUISwiftUI/Components/ChartComponents.swift @@ -0,0 +1,896 @@ +import Charts +import OpenUILang +import SwiftUI + +/// One value of one series at one category label. The id is the series and +/// label (not the position), so a value that changes while streaming moves to +/// its new place instead of being redrawn. +private struct SeriesPoint: Identifiable, Equatable { + let id: String + let label: String + let series: String + let value: Double +} + +/// Flattens `labels` + `[Series(category, values)]` into chart points. +/// Missing values (still streaming) are skipped. +private func seriesPoints(_ props: ComponentProps) -> [SeriesPoint] { + let labels = props.array("labels").map(displayText) + var points: [SeriesPoint] = [] + for series in props.children("series") { + let name = series.text("category") + for (index, value) in series.array("values").enumerated() where index < labels.count { + guard let number = value.numberValue, number.isFinite else { continue } + points.append( + SeriesPoint( + id: "\(name)\u{1}\(labels[index])\u{1}\(index)", label: labels[index], series: name, + value: number)) + } + } + return points +} + +/// Category labels in their given order (charts otherwise sort them). +private func labelOrder(_ props: ComponentProps) -> [String] { + var seen: Set = [] + return props.array("labels").map(displayText).filter { seen.insert($0).inserted } +} + +/// react-ui's `seriesCurve`: "linear", "step", and anything else (including a +/// value still streaming in) is the default "natural", a monotone curve. +private func interpolation(_ variant: String?) -> InterpolationMethod { + switch variant { + case "linear": return .linear + case "step": return .stepCenter + default: return .monotone + } +} + +/// Chart colors picked like react-ui's `getDistributedColors`: from the middle +/// of the ramp outwards. The ramp is the theme's `chartPalette`, or react-ui's +/// default (OCEAN_DEFAULT) when the theme has none. +enum ChartPalette { + static let ocean: [Color] = [ + 0x0D47A1, 0x1565C0, 0x1976D2, 0x1E88E5, 0x2196F3, 0x42A5F5, 0x64B5F6, 0x90CAF9, 0xBBDEFB, + 0xE3F2FD, 0xEFF8FF, + ].map(color) + + private static func color(_ hex: Int) -> Color { + let red = Double((hex >> 16) & 0xFF) / 255 + let green = Double((hex >> 8) & 0xFF) / 255 + let blue = Double(hex & 0xFF) / 255 + return Color(red: red, green: green, blue: blue) + } + + static func colors(_ count: Int, _ palette: [Color]? = nil) -> [Color] { + // An empty palette can't color anything, so it falls back like react-ui's. + let ramp = palette.flatMap { $0.isEmpty ? nil : $0 } ?? ocean + let n = ramp.count + let mid = n / 2 + switch count { + case ...0: return [] + case 1: return [ramp[mid]] + case 2: return [ramp[max(mid - 1, 0)], ramp[min(mid + 1, n - 1)]] + default: + let offset = (count - 1) / 2 + return (0.. [String] { + var seen: Set = [] + return props.children("series").map { $0.text("category") }.filter { seen.insert($0).inserted } +} + +/// A chart's series and their colors. Colors are assigned over every series, +/// so hiding one from the legend doesn't recolor the rest. +private struct SeriesColors { + let names: [String] + let colors: [Color] + + init(_ names: [String], _ palette: [Color]?) { + self.names = names + colors = ChartPalette.colors(names.count, palette) + } + + func color(_ name: String) -> Color { + names.firstIndex(of: name).map { colors[$0] } ?? .gray + } + + var legend: [LegendEntry] { + zip(names, colors).map { LegendEntry(id: $0, label: $0, color: $1) } + } +} + +/// The tooltip for one category: each visible series' value there. +private func seriesTooltip( + _ label: String, _ points: [SeriesPoint], _ series: SeriesColors +) -> ChartTooltipContent? { + let items = points.filter { $0.label == label }.map { + ChartTooltipContent.Item( + name: $0.series, value: ChartFormat.tooltip($0.value), color: series.color($0.series)) + } + return items.isEmpty ? nil : ChartTooltipContent(label: label, items: items) +} + +extension View { + /// Horizontal grid lines only, as react-ui's cartesian charts draw them. + /// The category axis: ticks, no grid lines, and react-ui's condensed + /// labels (see `CategoryLabels`). + fileprivate func categoryAxisWithoutGrid(_ labels: [String]) -> some View { + modifier(CategoryAxis(labels: labels)) + } + + /// react-ui's value axis: on the leading side, with grid lines and its tick + /// format (1.3K, 2.5M). + fileprivate func valueAxis(_ axis: Axis = .vertical) -> some View { + let marks = AxisMarks(position: axis == .vertical ? .leading : .bottom) { value in + AxisGridLine() + AxisValueLabel { + if let number = value.as(Double.self) { Text(ChartFormat.tick(number)) } + } + } + return Group { + if axis == .vertical { chartYAxis { marks } } else { chartXAxis { marks } } + } + } +} + +/// react-ui's condensed x-axis labels (`layoutXAxisLabels`): every category +/// shares the width, so when one is narrower than its label (counting at most +/// 40pt of it) plus an 8pt gap, only every n-th label is drawn, each truncated +/// to the room it then has. Without this, every label truncates to a letter or +/// two ("Mum…", "Kolk…") and similar ones ("Sprint 1", "Sprint 2") all read +/// the same. +enum CategoryLabels { + static let gap: CGFloat = 8 + static let minWidth: CGFloat = 40 + + /// Draw every `interval`-th label, each up to `width` wide. Past react-ui's + /// rule, it thins further while labels would still be cut off, as long as + /// at least three stay: "Month 0, Month 2, Month 4" reads better than seven + /// "Month…". + static func layout(widest: CGFloat, slot: CGFloat, count: Int) -> (interval: Int, width: CGFloat) + { + guard slot > 0, count > 0 else { return (1, slot) } + let needed = min(widest, minWidth) + gap + var interval = min(count, max(1, Int((needed / slot).rounded(.up)))) + let sparsest = max(interval, (count + 2) / 3) + while slot * CGFloat(interval) - gap < widest, interval < sparsest { interval += 1 } + return (interval, slot * CGFloat(interval) - gap) + } + + /// Where a label goes, as react-ui's CondensedXAxis draws it: in a box + /// `width` wide centered on its category, cut at the ends of the plot, with + /// the text truncated to the box and moved inside it. Returns the text's + /// width and its offset from the category's center. + static func place( + text: CGFloat, center: CGFloat, width: CGFloat, plot: CGFloat + ) -> (width: CGFloat, offset: CGFloat) { + let left = max(0, center - width / 2) + let right = min(plot, center + width / 2) + let shown = min(text, max(right - left, 0)) + let x = min(max(center, left + shown / 2), right - shown / 2) + return (shown, x - center) + } +} + +private struct CategoryAxis: ViewModifier { + let labels: [String] + @State private var width: CGFloat = 0 + @State private var plotWidth: CGFloat = 0 + + func body(content: Content) -> some View { + // Until the plot is measured, it's the chart less its value axis, about + // 36pt of labels. + let plot = plotWidth > 0 ? plotWidth : max(width - 36, 0) + let slot = labels.isEmpty ? 0 : plot / CGFloat(labels.count) + let widest = labels.map { ChartLegend.textWidth($0, style: .caption2) }.max() ?? 0 + let (interval, labelWidth) = CategoryLabels.layout( + widest: widest, slot: slot, count: labels.count) + let shown = labels.enumerated().filter { $0.offset % interval == 0 }.map(\.element) + content + .chartXAxis { + AxisMarks(values: width > 0 ? shown : labels) { value in + AxisTick() + // Swift Charts would fit each label to one category's width and + // center it there even past the ends of the chart. Each label gets + // the room left by the labels skipped instead, kept inside the plot. + AxisValueLabel(collisionResolution: .disabled) { + if let label = value.as(String.self) { + let text = ChartLegend.textWidth(label, style: .caption2) + 2 + let center = slot * (CGFloat(labels.firstIndex(of: label) ?? 0) + 0.5) + let (size, offset) = + width > 0 + ? CategoryLabels.place(text: text, center: center, width: labelWidth, plot: plot) + : (text, 0) + Text(label) + .font(.caption2) + .lineLimit(1) + .frame(width: size) + .fixedSize() + .offset(x: offset) + } + } + } + } + .chartPlotStyle { area in + area.onGeometryChange(for: CGFloat.self) { + $0.size.width + } action: { + plotWidth = $0 + } + } + .onGeometryChange(for: CGFloat.self) { + $0.size.width + } action: { + width = $0 + } + } +} + +/// Sizes a chart and puts react-ui's legend under it, or shows a pulsing +/// placeholder while the response streams in before any of its values have. +private struct ChartFrame: View { + let props: ComponentProps + let isEmpty: Bool + let legend: [LegendEntry] + let hidden: Binding>? + /// The least height the chart needs, e.g. a row per bar. + let minHeight: CGFloat + let content: Content + @Environment(OpenUIContext.self) private var context + @Environment(\.openUITheme) private var theme + + init( + _ props: ComponentProps, isEmpty: Bool, legend: [LegendEntry], + hidden: Binding>? = nil, minHeight: CGFloat = 0, + @ViewBuilder content: () -> Content + ) { + self.props = props + self.isEmpty = isEmpty + self.legend = legend + self.hidden = hidden + self.minHeight = minHeight + self.content = content() + } + + var body: some View { + let height = max( + props.number("height").flatMap(\.finite).map { CGFloat($0) } ?? theme.chartHeight, minHeight) + if isEmpty && (context.isStreaming || context.isQueryLoading) { + SkeletonBlock(height: height, cornerRadius: 8) + } else { + VStack(spacing: 12) { + AxisTitles(props) { + content + .frame(height: height) + .chartLegend(.hidden) + } + if !legend.isEmpty { ChartLegend(entries: legend, hidden: hidden) } + } + } + } +} + +/// react-ui's condensed charts put the axis titles outside the plot: the y +/// title above it, the x title centered below. Outside the chart, a missing +/// title takes no room (Swift Charts keeps room for an empty one), and one +/// that streams in after the data doesn't rebuild the chart. +private struct AxisTitles: View { + let x: String + let y: String + let content: Content + + init(_ props: ComponentProps, @ViewBuilder content: () -> Content) { + x = props.text("xLabel") + y = props.text("yLabel") + self.content = content() + } + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + if !y.isEmpty { + Text(y).font(.caption).foregroundStyle(.secondary).padding(.bottom, 8) + } + content + if !x.isEmpty { + Text(x).font(.caption).foregroundStyle(.secondary).frame(maxWidth: .infinity) + } + } + } +} + +struct BarChartView: View { + let props: ComponentProps + @State private var hidden: Set = [] + @State private var selection: ChartSelection? + @Environment(\.openUITheme) private var theme + + var body: some View { + let all = seriesPoints(props) + let points = all.filter { !hidden.contains($0.series) } + let series = SeriesColors(seriesNames(props), theme.chartRamp(theme.barChartPalette)) + let labels = labelOrder(props) + let stacked = props.string("variant") == "stacked" + ChartFrame(props, isEmpty: all.isEmpty, legend: series.legend, hidden: $hidden) { + Chart(points) { point in + if stacked { + BarMark(x: .value("Label", point.label), y: .value("Value", point.value)) + .foregroundStyle(by: .value("Series", point.series)) + } else { + BarMark(x: .value("Label", point.label), y: .value("Value", point.value)) + .foregroundStyle(by: .value("Series", point.series)) + .position(by: .value("Series", point.series)) + .cornerRadius(4) + } + } + .chartXScale(domain: labels) + .chartForegroundStyleScale(domain: series.names, range: series.colors) + .categoryAxisWithoutGrid(labelOrder(props)) + .valueAxis() + .categoryPicker( + $selection, crosshairBehind: true, tooltip: { seriesTooltip($0, points, series) } + ) { proxy, plot, label in + if let x = proxy.position(forX: label) { + BandHighlight(center: x, slot: plot.width / CGFloat(max(labels.count, 1)), plot: plot) + } + } + .chartEntrance(.grow) + .openUIAnimation(Motion.dataMorph, value: points) + } + } +} + +struct HorizontalBarChartView: View { + let props: ComponentProps + @State private var hidden: Set = [] + @State private var selection: ChartSelection? + @Environment(\.openUITheme) private var theme + + var body: some View { + let all = seriesPoints(props) + let points = all.filter { !hidden.contains($0.series) } + let series = SeriesColors(seriesNames(props), theme.chartRamp(theme.horizontalBarChartPalette)) + let stacked = props.string("variant") == "stacked" + let labels = labelOrder(props) + // A row per bar, at least 28pt each: past that the frame grows instead of + // the bars spilling over the axis titles. + ChartFrame( + props, isEmpty: all.isEmpty, legend: series.legend, hidden: $hidden, + minHeight: CGFloat(labels.count) * 28 + ) { + Chart(points) { point in + if stacked { + BarMark( + x: .value("Value", point.value), y: .value("Label", point.label), height: .ratio(0.7) + ) + .foregroundStyle(by: .value("Series", point.series)) + } else { + BarMark( + x: .value("Value", point.value), y: .value("Label", point.label), height: .ratio(0.7) + ) + .foregroundStyle(by: .value("Series", point.series)) + .position(by: .value("Series", point.series)) + .cornerRadius(4) + } + } + .chartYScale(domain: labels) + .chartForegroundStyleScale(domain: series.names, range: series.colors) + .chartYAxis { + AxisMarks(preset: .aligned, position: .leading) { _ in + AxisValueLabel(horizontalSpacing: 8) + } + } + .valueAxis(.horizontal) + .categoryPicker( + $selection, axis: .vertical, crosshairBehind: true, + tooltip: { seriesTooltip($0, points, series) } + ) { proxy, plot, label in + if let y = proxy.position(forY: label) { + BandHighlight( + center: y, slot: plot.height / CGFloat(max(labels.count, 1)), plot: plot, + axis: .vertical) + } + } + .chartEntrance(.growSideways) + .openUIAnimation(Motion.dataMorph, value: points) + } + } +} + +struct LineChartView: View { + let props: ComponentProps + @State private var hidden: Set = [] + @State private var selection: ChartSelection? + @Environment(\.openUITheme) private var theme + + var body: some View { + let method = interpolation(props.string("variant")) + let all = seriesPoints(props) + let points = all.filter { !hidden.contains($0.series) } + let series = SeriesColors(seriesNames(props), theme.chartRamp(theme.lineChartPalette)) + ChartFrame(props, isEmpty: all.isEmpty, legend: series.legend, hidden: $hidden) { + Chart(points) { point in + LineMark(x: .value("Label", point.label), y: .value("Value", point.value)) + .foregroundStyle(by: .value("Series", point.series)) + .interpolationMethod(method) + .lineStyle(StrokeStyle(lineWidth: 2)) + } + .chartXScale(domain: labelOrder(props)) + .chartForegroundStyleScale(domain: series.names, range: series.colors) + .categoryAxisWithoutGrid(labelOrder(props)) + .valueAxis() + .categoryPicker($selection, tooltip: { seriesTooltip($0, points, series) }) { + proxy, plot, label in + lineCrosshair(proxy, plot, label, points, series) + } + .chartEntrance(.draw) + .openUIAnimation(Motion.dataMorph, value: points) + } + } +} + +@ViewBuilder +private func lineCrosshair( + _ proxy: ChartProxy, _ plot: CGRect, _ label: String, _ points: [SeriesPoint], + _ series: SeriesColors +) -> some View { + if let x = proxy.position(forX: label) { + LineCrosshair( + x: x, plot: plot, + dots: points.filter { $0.label == label }.compactMap { point in + proxy.position(forY: point.value).map { (y: $0, color: series.color(point.series)) } + }) + } +} + +struct AreaChartView: View { + let props: ComponentProps + @State private var hidden: Set = [] + @State private var selection: ChartSelection? + @Environment(\.openUITheme) private var theme + + var body: some View { + let method = interpolation(props.string("variant")) + let series = SeriesColors(seriesNames(props), theme.chartRamp(theme.areaChartPalette)) + let all = seriesPoints(props) + let points = all.filter { !hidden.contains($0.series) } + ChartFrame(props, isEmpty: all.isEmpty, legend: series.legend, hidden: $hidden) { + Chart(points) { point in + let color = series.color(point.series) + // react-ui fills areas with a gradient from 60% opacity to clear. + AreaMark( + x: .value("Label", point.label), y: .value("Value", point.value), + series: .value("Series", point.series), stacking: .unstacked + ) + .foregroundStyle( + LinearGradient( + colors: [color.opacity(0.6), color.opacity(0)], startPoint: .top, endPoint: .bottom) + ) + .interpolationMethod(method) + LineMark(x: .value("Label", point.label), y: .value("Value", point.value)) + .foregroundStyle(by: .value("Series", point.series)) + .interpolationMethod(method) + .lineStyle(StrokeStyle(lineWidth: 2)) + } + .chartXScale(domain: labelOrder(props)) + .chartForegroundStyleScale(domain: series.names, range: series.colors) + .categoryAxisWithoutGrid(labelOrder(props)) + .valueAxis() + .categoryPicker($selection, tooltip: { seriesTooltip($0, points, series) }) { + proxy, plot, label in + lineCrosshair(proxy, plot, label, points, series) + } + .chartEntrance(.draw) + .openUIAnimation(Motion.dataMorph, value: points) + } + } +} + +/// One labelled value of a 1D chart. +struct Slice: Identifiable, Equatable { + let id: Int + let label: String + let value: Double +} + +private func slices(_ props: ComponentProps) -> [Slice] { + let labels = props.array("labels").map(displayText) + return props.array("values").enumerated().compactMap { index, value in + guard index < labels.count, let number = value.numberValue, number.isFinite, number >= 0 else { + return nil + } + return Slice(id: index, label: labels[index], value: number) + } +} + +/// Pie and radial slices as react-ui orders them: largest first (a stable +/// sort, like JavaScript's), each with its color from the middle of the ramp +/// outwards in that order. +func sortedSlices(_ slices: [Slice], _ palette: [Color]?) -> [(slice: Slice, color: Color)] { + let sorted = slices.enumerated().sorted { + $0.element.value != $1.element.value + ? $0.element.value > $1.element.value : $0.offset < $1.offset + }.map(\.element) + return Array(zip(sorted, ChartPalette.colors(sorted.count, palette))) +} + +/// Where pie slices are: Swift Charts starts at 12 o'clock and goes clockwise, +/// with the radius half the shorter side. +enum PieGeometry { + /// The index of the slice under `point`, or nil outside the ring. A + /// semicircle is drawn turned a quarter to the left, so its angles are too. + static func slice( + at point: CGPoint, in size: CGSize, values: [Double], innerRatio: CGFloat, semicircle: Bool + ) -> Int? { + let dx = point.x - size.width / 2 + let dy = point.y - size.height / 2 + let radius = min(size.width, size.height) / 2 + let distance = (dx * dx + dy * dy).squareRoot() + guard distance <= radius, distance >= radius * innerRatio else { return nil } + var angle = atan2(Double(dx), Double(-dy)) + (semicircle ? Double.pi / 2 : 0) + angle = angle.truncatingRemainder(dividingBy: 2 * .pi) + if angle < 0 { angle += 2 * .pi } + let total = values.reduce(0, +) * (semicircle ? 2 : 1) + guard total > 0 else { return nil } + var end = 0.0 + for (index, value) in values.enumerated() { + end += value / total * 2 * .pi + if angle < end { return index } + } + return nil + } +} + +struct PieChartView: View { + let props: ComponentProps + @State private var hidden: Set = [] + @State private var selection: ChartSelection? + @State private var legendHover: String? + @Environment(\.openUITheme) private var theme + + var body: some View { + let all = sortedSlices(slices(props), theme.chartRamp(theme.pieChartPalette)) + let visible = all.filter { !hidden.contains(String($0.slice.id)) } + let donut = props.string("variant") == "donut" + let semi = props.string("appearance") == "semiCircular" + let total = visible.reduce(0) { $0 + $1.slice.value } + let active = selection?.key ?? legendHover + VStack(spacing: 12) { + Chart { + ForEach(visible, id: \.slice.id) { slice, color in + SectorMark( + angle: .value("Value", slice.value), innerRadius: .ratio(donut ? 0.6 : 0), + angularInset: 1 + ) + .foregroundStyle(color) + .opacity(active == nil || active == String(slice.id) ? 1 : 0.4) + .accessibilityLabel(slice.label) + .accessibilityValue(ChartFormat.tooltip(slice.value)) + } + if semi, total > 0 { + // A transparent half that keeps the visible slices on the top half. + SectorMark(angle: .value("Value", total), innerRadius: .ratio(donut ? 0.6 : 0)) + .foregroundStyle(.clear) + } + } + .rotationEffect(semi ? .degrees(-90) : .zero) + .overlay { + GeometryReader { geometry in + ZStack(alignment: .topLeading) { + PickSurface(selection: $selection) { point in + PieGeometry.slice( + at: point, in: geometry.size, values: visible.map(\.slice.value), + innerRatio: donut ? 0.6 : 0, semicircle: semi + ).map { String(visible[$0].slice.id) } + } + if let key = selection?.key, let anchor = selection?.location, + let (slice, color) = visible.first(where: { String($0.slice.id) == key }) + { + TooltipPlacement(anchor: anchor) { + ChartTooltip( + content: ChartTooltipContent( + label: slice.label, + items: [ + .init( + name: slice.label, value: ChartFormat.tooltip(slice.value), color: color) + ] + )) + } + .allowsHitTesting(false) + } + } + } + } + .frame(height: theme.chartHeight) + .openUIAnimation(Motion.dataMorph, value: visible.map(\.slice)) + .fadeAppear() + ChartLegend( + entries: all.map { + LegendEntry(id: String($0.slice.id), label: $0.slice.label, color: $0.color) + }, + hidden: $hidden, highlighted: $legendHover) + } + } +} + +struct RadialChartView: View { + let props: ComponentProps + @State private var hidden: Set = [] + @Environment(\.openUITheme) private var theme + + var body: some View { + let all = sortedSlices(slices(props), theme.chartRamp(theme.radialChartPalette)) + let visible = all.filter { !hidden.contains(String($0.slice.id)) } + let maximum = max(visible.map(\.slice.value).max() ?? 1, 1) + VStack(spacing: 12) { + ZStack { + ForEach(Array(visible.enumerated()), id: \.element.slice.id) { ring, entry in + let inset = CGFloat(ring) * 14 + Circle() + .stroke(.secondary.opacity(0.15), lineWidth: 10) + .padding(inset) + Circle() + .trim(from: 0, to: entry.slice.value / maximum * 0.75) + .stroke(entry.color, style: StrokeStyle(lineWidth: 10, lineCap: .round)) + .rotationEffect(.degrees(-90)) + .padding(inset) + .accessibilityElement() + .accessibilityLabel(entry.slice.label) + .accessibilityValue(jsNumberToString(entry.slice.value)) + } + } + .frame(width: theme.chartHeight * 0.8, height: theme.chartHeight * 0.8) + .frame(maxWidth: .infinity) + ChartLegend( + entries: all.map { + LegendEntry(id: String($0.slice.id), label: $0.slice.label, color: $0.color) + }, + hidden: $hidden) + } + .openUIAnimation(Motion.dataMorph, value: visible.map(\.slice)) + .fadeAppear() + } +} + +/// react-ui's SingleStackedBar (its SegmentedBar): one rounded track split by +/// share, colored down the ramp in the given order, with each segment's share +/// in the legend. Hovering or tapping a segment (or its key) dims the rest +/// and shows its value and share. +struct SingleStackedBarChartView: View { + let props: ComponentProps + @State private var selection: ChartSelection? + @State private var legendHover: String? + @Environment(\.openUITheme) private var theme + + var body: some View { + let data = slices(props) + let ramp = theme.chartRamp(theme.barChartPalette) ?? ChartPalette.ocean + let total = data.reduce(0) { $0 + $1.value } + let share = { (slice: Slice) in total > 0 ? slice.value / total : 0 } + let color = { (slice: Slice) in ramp[slice.id % ramp.count] } + let active = selection?.key ?? legendHover + VStack(spacing: 12) { + SegmentRow(shares: data.map(share)) { + ForEach(data) { slice in + Rectangle() + .fill(color(slice)) + .opacity(active == nil || active == String(slice.id) ? 1 : 0.4) + .accessibilityElement() + .accessibilityLabel(slice.label) + .accessibilityValue(ChartFormat.toFixed(share(slice) * 100, 1) + "%") + } + } + .frame(height: 20) + .clipShape(RoundedRectangle(cornerRadius: 6)) + .overlay { + GeometryReader { geometry in + ZStack(alignment: .topLeading) { + PickSurface(selection: $selection) { point in + SegmentRow.index(at: point.x, width: geometry.size.width, shares: data.map(share)) + .map { String(data[$0].id) } + } + if let key = selection?.key, let anchor = selection?.location, + let slice = data.first(where: { String($0.id) == key }) + { + TooltipPlacement(anchor: anchor, placement: .above) { + ChartTooltip( + content: ChartTooltipContent( + label: slice.label, + items: [ + .init( + name: "Value", value: ChartFormat.tick(slice.value), color: color(slice)), + .init( + name: "Percentage", value: ChartFormat.toFixed(share(slice) * 100, 1) + "%", + color: color(slice)), + ])) + } + .allowsHitTesting(false) + } + } + } + } + .openUIAnimation(Motion.dataMorph, value: data) + .chartEntrance(.growSideways) + ChartLegend( + entries: data.map { + LegendEntry( + id: String($0.id), label: $0.label, color: color($0), percentage: share($0) * 100) + }, + highlighted: $legendHover) + } + } +} + +/// Lays segments out side by side, each as wide as its share of the row, +/// with react-ui's 2pt gaps. +struct SegmentRow: Layout { + let shares: [Double] + static let gap: CGFloat = 2 + + func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { + proposal.replacingUnspecifiedDimensions() + } + + func placeSubviews( + in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout () + ) { + var x = bounds.minX + for (subview, width) in zip(subviews, Self.widths(bounds.width, shares)) { + subview.place( + at: CGPoint(x: x, y: bounds.minY), + proposal: ProposedViewSize(width: width, height: bounds.height)) + x += width + Self.gap + } + } + + static func widths(_ width: CGFloat, _ shares: [Double]) -> [CGFloat] { + let room = max(width - gap * CGFloat(max(shares.count - 1, 0)), 0) + return shares.map { room * CGFloat($0) } + } + + /// The segment at `x`, counting each gap with the segment before it. + static func index(at x: CGFloat, width: CGFloat, shares: [Double]) -> Int? { + var end: CGFloat = 0 + for (index, segment) in widths(width, shares).enumerated() { + end += segment + gap + if x < end, segment > 0 { return index } + } + return nil + } +} + +struct ScatterChartView: View { + let props: ComponentProps + @State private var hidden: Set = [] + @Environment(\.openUITheme) private var theme + + private struct Dot: Identifiable, Equatable { + let id: Int + let series: String + let x: Double + let y: Double + let z: Double? + } + + var body: some View { + var dots: [Dot] = [] + for dataset in props.children("datasets") { + for point in dataset.children("points") { + // Like the other charts, a point that isn't a finite number is skipped. + guard let x = point.number("x").flatMap(\.finite), + let y = point.number("y").flatMap(\.finite) + else { continue } + dots.append( + Dot(id: dots.count, series: dataset.text("name"), x: x, y: y, z: point.number("z"))) + } + } + let series = SeriesColors( + props.children("datasets").map { $0.text("name") }, theme.chartPalette) + let visible = dots.filter { !hidden.contains($0.series) } + return VStack(spacing: 12) { + AxisTitles(props) { + scatter(visible, all: dots, series: series) + } + ChartLegend(entries: series.legend, hidden: $hidden) + } + } + + private func scatter(_ visible: [Dot], all dots: [Dot], series: SeriesColors) -> some View { + Chart(visible) { dot in + PointMark(x: .value(props.text("xLabel"), dot.x), y: .value(props.text("yLabel"), dot.y)) + .foregroundStyle(by: .value("Series", dot.series)) + .symbolSize(dot.z.map { max(20, min($0, 400)) } ?? 40) + } + .chartXScale(domain: Self.domain(dots.map(\.x))) + .chartYScale(domain: Self.domain(dots.map(\.y))) + .chartForegroundStyleScale(domain: series.names, range: series.colors) + .chartLegend(.hidden) + .frame(height: theme.chartHeight) + .openUIAnimation(Motion.dataMorph, value: visible) + .fadeAppear() + } + + /// react-ui's `calculateScatterDomain`: the data range padded by 10% on each + /// side, never below zero. + static func domain(_ values: [Double]) -> ClosedRange { + guard let low = values.min(), let high = values.max() else { return 0...100 } + let padding = (high - low) * 0.1 + let start = max(0, low - padding) + let end = high + padding + return end > start ? start...end : start...(start + 1) + } +} + +/// Spider chart: one axis per label, one filled polygon per series. Swift +/// Charts has no radar mark, so it is drawn directly. +struct RadarChartView: View { + let props: ComponentProps + @State private var hidden: Set = [] + @Environment(\.openUITheme) private var theme + + var body: some View { + let labels = props.array("labels").map(displayText) + // A value that isn't a finite number draws at the center, so it can't + // become the maximum that scales every other value to nothing. + let all = props.children("series").map { series in + ( + name: series.text("category"), + values: series.array("values").map { $0.numberValue.flatMap(\.finite) ?? 0 } + ) + } + let colors = SeriesColors(all.map(\.name), theme.chartRamp(theme.radarChartPalette)) + let series = all.filter { !hidden.contains($0.name) } + let maximum = max(series.flatMap(\.values).max() ?? 1, 1) + VStack(spacing: 12) { + Canvas { context, size in + guard labels.count >= 3 else { return } + let center = CGPoint(x: size.width / 2, y: size.height / 2) + let radius = min(size.width, size.height) / 2 - 28 + func point(_ index: Int, _ fraction: Double) -> CGPoint { + let angle = -Double.pi / 2 + 2 * Double.pi * Double(index) / Double(labels.count) + return CGPoint( + x: center.x + CGFloat(cos(angle) * fraction) * radius, + y: center.y + CGFloat(sin(angle) * fraction) * radius) + } + func polygon(_ fractions: [Double]) -> Path { + Path { path in + for (index, fraction) in fractions.enumerated() { + index == 0 + ? path.move(to: point(index, fraction)) : path.addLine(to: point(index, fraction)) + } + path.closeSubpath() + } + } + for ring in 1...4 { + context.stroke( + polygon(Array(repeating: Double(ring) / 4, count: labels.count)), + with: .color(.secondary.opacity(0.25))) + } + for index in labels.indices { + var axis = Path() + axis.move(to: center) + axis.addLine(to: point(index, 1)) + context.stroke(axis, with: .color(.secondary.opacity(0.25))) + context.draw( + Text(labels[index]).font(.caption2).foregroundStyle(.secondary), at: point(index, 1.16)) + } + for entry in series { + let fractions = labels.indices.map { + $0 < entry.values.count ? entry.values[$0] / maximum : 0 + } + let shape = polygon(fractions) + let color = colors.color(entry.name) + context.fill(shape, with: .color(color.opacity(0.18))) + context.stroke(shape, with: .color(color), lineWidth: 2) + } + } + .frame(height: theme.chartHeight + 40) + .fadeAppear() + ChartLegend(entries: colors.legend, hidden: $hidden) + } + } +} diff --git a/packages/swift-lang/Sources/OpenUISwiftUI/Components/ChartInteraction.swift b/packages/swift-lang/Sources/OpenUISwiftUI/Components/ChartInteraction.swift new file mode 100644 index 0000000000..ed565a5f10 --- /dev/null +++ b/packages/swift-lang/Sources/OpenUISwiftUI/Components/ChartInteraction.swift @@ -0,0 +1,459 @@ +import Charts +import OpenUILang +import SwiftUI + +// MARK: - Number formats + +/// The number formats react-ui's charts use. +enum ChartFormat { + /// `numberTickFormatter`, for value axes: 950, 1.3K, 16K, 2.5M; fractions + /// with two decimals. + static func tick(_ value: Double) -> String { + let magnitude = abs(value) + for (limit, suffix) in [(1e12, "T"), (1e9, "B"), (1e6, "M"), (1e3, "K")] + where magnitude >= limit { + return toFixed(value / limit, magnitude >= limit * 10 ? 0 : 1) + suffix + } + return value.truncatingRemainder(dividingBy: 1) != 0 + ? toFixed(value, 2) : jsNumberToString(value) + } + + /// `tooltipNumberFormatter`: grouped like `toLocaleString` below 100,000, + /// then 123.4K, 5.6M and so on, rounded down to one decimal. + static func tooltip(_ value: Double, locale: Locale = .current) -> String { + let sign = value < 0 ? "-" : "" + var scaled = abs(value) + if scaled < 100_000 { + return sign + + scaled.formatted( + .number.locale(locale).precision(.fractionLength(0...3)) + .rounded(rule: .toNearestOrAwayFromZero)) + } + let units = ["", "K", "M", "B", "T"] + var unit = 0 + while scaled >= 1000 && unit < units.count - 1 { + scaled /= 1000 + unit += 1 + } + return sign + jsNumberToString((scaled * 10).rounded(.down) / 10) + units[unit] + } + + /// JavaScript's `toFixed`. `printf` rounds a value exactly halfway between + /// two results to even; JavaScript rounds it away from zero (0.125 → "0.13"). + static func toFixed(_ value: Double, _ digits: Int) -> String { + let scale = pow(10, Double(digits)) + let scaled = abs(value) * scale + if scaled.truncatingRemainder(dividingBy: 1) == 0.5, scaled / scale == abs(value) { + let rounded = scaled.rounded(.awayFromZero) / scale + return String(format: "%.\(digits)f", value < 0 ? -rounded : rounded) + } + return String(format: "%.\(digits)f", value) + } +} + +// MARK: - Legend + +/// One key of a chart legend. +struct LegendEntry: Identifiable, Equatable { + let id: String + let label: String + let color: Color + var percentage: Double? +} + +/// react-ui's DefaultLegend: centered color keys under a chart. With +/// `hidden`, tapping a key hides its series (one always stays visible); with +/// `highlighted`, hovering a key highlights it in the chart. Keys that don't +/// fit on one line fold behind an "N more" button. +struct ChartLegend: View { + let entries: [LegendEntry] + var hidden: Binding>? + var highlighted: Binding? + @State private var expanded = false + @State private var width: CGFloat = 0 + + var body: some View { + Group { + if width == 0 { + // One clipped line until the width is known, the height the folded + // legend has, so nothing jumps when it folds. + HStack(spacing: Self.gap) { + ForEach(entries) { item($0).fixedSize() } + } + .frame(maxWidth: .infinity) + .clipped() + } else { + let fitting = Self.fittingCount( + entries.map(Self.width), available: width, + button: Self.buttonWidth(entries.count)) + FlowLayout(spacing: Self.gap, alignment: .center) { + ForEach(entries.prefix(expanded ? entries.count : fitting)) { item($0) } + if fitting < entries.count { + Button { + withOpenUIAnimation(Motion.reveal) { expanded.toggle() } + } label: { + HStack(spacing: 2) { + Text(expanded ? "Show Less" : "\(entries.count - fitting) more") + Image(systemName: expanded ? "chevron.up" : "chevron.down").imageScale(.small) + } + .font(.caption) + } + .buttonStyle(.plain) + } + } + .frame(maxWidth: .infinity) + } + } + .onGeometryChange(for: CGFloat.self) { + $0.size.width + } action: { + width = $0 + } + } + + @ViewBuilder + private func item(_ entry: LegendEntry) -> some View { + let isHidden = hidden?.wrappedValue.contains(entry.id) ?? false + let key = HStack(spacing: 6) { + RoundedRectangle(cornerRadius: 2).fill(entry.color).frame(width: 10, height: 10) + Text(entry.label).font(.caption) + if let percentage = entry.percentage { + Text(ChartFormat.toFixed(percentage, 1) + "%").font(.caption).foregroundStyle(.secondary) + } + } + .opacity(isHidden ? 0.3 : 1) + .contentShape(Rectangle()) + .onHover { inside in highlighted?.wrappedValue = inside ? entry.id : nil } + if let hidden { + Button { + withOpenUIAnimation(Motion.dataMorph) { + hidden.wrappedValue = Self.toggling( + entry.id, in: hidden.wrappedValue, keys: entries.map(\.id)) + } + } label: { + key + } + .buttonStyle(.plain) + .accessibilityValue(isHidden ? "Hidden" : "Shown") + } else { + key.accessibilityElement(children: .combine) + } + } + + /// The gap between keys, react-ui's `space-m`. + static let gap: CGFloat = 12 + + /// react-ui's `useSeriesVisibility`: hides or shows `key`, but never hides + /// the last visible series. + static func toggling(_ key: String, in hidden: Set, keys: [String]) -> Set { + var next = hidden + if next.contains(key) { + next.remove(key) + } else { + guard keys.filter(next.contains).count < keys.count - 1 else { return hidden } + next.insert(key) + } + return next + } + + /// react-ui's `useDefaultLegend`: how many keys fit on one line. All of them + /// if they fit the full width; otherwise as many as fit next to the button, + /// and at least one. + static func fittingCount(_ widths: [CGFloat], available: CGFloat, button: CGFloat) -> Int { + guard available > 0, !widths.isEmpty else { return widths.count } + func count(_ space: CGFloat) -> Int { + var used: CGFloat = 0 + var fitting = 0 + for width in widths { + let needed = fitting > 0 ? width + gap : width + guard used + needed <= space else { break } + used += needed + fitting += 1 + } + return fitting + } + if count(available) == widths.count { return widths.count } + return max(count(available - button), 1) + } + + /// A key's width as react-ui measures it: the text, the 10pt swatch and a gap. + private static func width(_ entry: LegendEntry) -> CGFloat { + var text = entry.label + if let percentage = entry.percentage { text += " (\(ChartFormat.toFixed(percentage, 1))%)" } + return textWidth(text) + 10 + gap + } + + private static func buttonWidth(_ count: Int) -> CGFloat { + textWidth("\(count) more") + 20 + } + + static func textWidth(_ text: String, style: Font.TextStyle = .caption) -> CGFloat { + #if os(macOS) + let font = NSFont.preferredFont(forTextStyle: style == .caption2 ? .caption2 : .caption1) + #else + let font = UIFont.preferredFont(forTextStyle: style == .caption2 ? .caption2 : .caption1) + #endif + return ceil((text as NSString).size(withAttributes: [.font: font]).width) + } +} + +// MARK: - Tooltip + +/// What a chart's tooltip lists: react-ui's `{ label, items }` payload. +struct ChartTooltipContent: Equatable { + struct Item: Equatable { + let name: String + let value: String + let color: Color + } + + let label: String + let items: [Item] +} + +/// react-ui's ChartTooltip: the label, then a swatch, name and value for each +/// item. One or two items stack each name over its value; more go in rows. Past +/// ten, the first five show with a count of the rest. +struct ChartTooltip: View { + let content: ChartTooltipContent + @Environment(\.openUITheme) private var theme + + var body: some View { + let items = content.items + let shown = items.count > 10 ? Array(items.prefix(5)) : items + VStack(alignment: .leading, spacing: 6) { + Text(content.label) + Divider() + ForEach(Array(shown.enumerated()), id: \.offset) { _, item in + let swatch = RoundedRectangle(cornerRadius: 2).fill(item.color).frame(width: 10, height: 10) + if items.count <= 2 { + HStack(alignment: .top, spacing: 6) { + swatch.padding(.top, 2) + VStack(alignment: .leading, spacing: 4) { + Text(item.name).foregroundStyle(.secondary) + Text(item.value).monospacedDigit() + } + } + } else { + HStack(spacing: 6) { + swatch + Text(item.name).foregroundStyle(.secondary).lineLimit(1) + Spacer(minLength: 8) + Text(item.value).monospacedDigit().lineLimit(1) + } + } + } + if items.count > 10 { + Divider() + Text("\(items.count - 5) more").foregroundStyle(.secondary) + } + } + .font(.caption) + .padding(6) + .frame(minWidth: 128, maxWidth: 240, alignment: .leading) + .fixedSize() + .background(theme.surface, in: RoundedRectangle(cornerRadius: 10)) + .overlay(RoundedRectangle(cornerRadius: 10).strokeBorder(theme.border)) + .shadow(color: .black.opacity(0.08), radius: 4, y: 2) + } +} + +/// Places a tooltip like react-ui's floating one: 20pt right of the anchor, +/// flipped to the left when it would run off the trailing edge, and kept 8pt +/// inside the bounds. `.above` centers it 20pt over the anchor instead, as +/// the stacked bar does. +struct TooltipPlacement: Layout { + enum Placement { case trailing, above } + + let anchor: CGPoint + var placement = Placement.trailing + + func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { + proposal.replacingUnspecifiedDimensions() + } + + func placeSubviews( + in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout () + ) { + for subview in subviews { + let size = subview.sizeThatFits(.unspecified) + subview.place( + at: Self.origin(for: size, anchor: anchor, placement: placement, in: bounds), + proposal: ProposedViewSize(size)) + } + } + + static func origin( + for size: CGSize, anchor: CGPoint, placement: Placement = .trailing, in bounds: CGRect + ) -> CGPoint { + if placement == .above { + let x = min( + max(bounds.minX + anchor.x - size.width / 2, bounds.minX + 8), bounds.maxX - 8 - size.width) + return CGPoint(x: max(x, bounds.minX), y: bounds.minY + anchor.y - 20 - size.height) + } + var x = bounds.minX + anchor.x + 20 + if x + size.width > bounds.maxX - 8 { x = bounds.minX + anchor.x - 20 - size.width } + x = max(bounds.minX + 8, x) + let y = min( + max(bounds.minY + anchor.y, bounds.minY), max(bounds.maxY - size.height, bounds.minY)) + return CGPoint(x: x, y: y) + } +} + +// MARK: - Picking a category + +/// A category picked by hovering or tapping a chart, and where. +struct ChartSelection: Equatable { + var key: String + var location: CGPoint +} + +/// A clear surface that turns hovering with a pointer (react-ui's mouse move) +/// and tapping (its touch handlers) into a selection: `pick` maps the point to +/// a key. Tapping the same key again clears it, and so does opening another +/// chart's tooltip. A tap doesn't stop a scroll view around the chart from +/// scrolling, which a drag would. +struct PickSurface: View { + @Binding var selection: ChartSelection? + let pick: (CGPoint) -> String? + @State private var hovering = false + @State private var id = UUID() + @Environment(OpenUIContext.self) private var context + + var body: some View { + Rectangle() + .fill(.clear) + .contentShape(Rectangle()) + .onContinuousHover { phase in + switch phase { + case .active(let location): + hovering = true + selection = choose(location) + case .ended: + hovering = false + selection = nil + } + } + .onTapGesture { location in + let picked = choose(location) + // Under a pointer, a click lands on what hovering already picked. + selection = !hovering && picked?.key == selection?.key ? nil : picked + } + .accessibilityHidden(true) + .onChange(of: selection) { _, selection in + if selection != nil { context.chartWithTooltip = id } + } + .onChange(of: context.chartWithTooltip) { _, chart in + if chart != id { selection = nil } + } + } + + private func choose(_ location: CGPoint) -> ChartSelection? { + pick(location).map { ChartSelection(key: $0, location: location) } + } +} + +/// Picks what's under the pointer or a tap on a Swift Charts chart, then +/// draws `crosshair` for it (behind or over the marks) and a tooltip. +struct ChartPicker: ViewModifier { + var crosshairBehind = false + let pick: (CGPoint, ChartProxy, CGRect) -> String? + let tooltip: (String) -> ChartTooltipContent? + @Binding var selection: ChartSelection? + @ViewBuilder let crosshair: (ChartProxy, CGRect, String) -> Crosshair + + func body(content: Content) -> some View { + content + .chartBackground { proxy in + GeometryReader { geometry in + if crosshairBehind, let key = selection?.key, let frame = proxy.plotFrame { + crosshair(proxy, geometry[frame], key) + } + } + } + .chartOverlay { proxy in + GeometryReader { geometry in + let plot = proxy.plotFrame.map { geometry[$0] } ?? .zero + ZStack(alignment: .topLeading) { + if !crosshairBehind, let key = selection?.key { + crosshair(proxy, plot, key) + } + PickSurface(selection: $selection) { location in + plot.contains(location) ? pick(location, proxy, plot) : nil + } + if let selection, let content = tooltip(selection.key) { + TooltipPlacement(anchor: selection.location) { ChartTooltip(content: content) } + .allowsHitTesting(false) + } + } + } + } + } +} + +extension View { + /// Picks the category under the pointer or a tap on a chart with + /// categories along `axis`. + func categoryPicker( + _ selection: Binding, axis: Axis = .horizontal, crosshairBehind: Bool = false, + tooltip: @escaping (String) -> ChartTooltipContent?, + @ViewBuilder crosshair: @escaping (ChartProxy, CGRect, String) -> Crosshair + ) -> some View { + modifier( + ChartPicker( + crosshairBehind: crosshairBehind, + pick: { location, proxy, plot in + axis == .horizontal + ? proxy.value(atX: location.x - plot.minX, as: String.self) + : proxy.value(atY: location.y - plot.minY, as: String.self) + }, + tooltip: tooltip, selection: selection, crosshair: crosshair)) + } +} + +/// react-ui's bar crosshair: a faint rounded band behind the picked category, +/// as wide as its bars' band. +struct BandHighlight: View { + let center: CGFloat + let slot: CGFloat + let plot: CGRect + var axis: Axis = .horizontal + + var body: some View { + let band = RoundedRectangle(cornerRadius: 4).fill(Color.primary.opacity(0.04)) + if axis == .horizontal { + band.frame(width: slot * 0.8, height: plot.height).position( + x: plot.minX + center, y: plot.midY) + } else { + band.frame(width: plot.width, height: slot * 0.8).position( + x: plot.midX, y: plot.minY + center) + } + } +} + +/// react-ui's line and area crosshair: a dashed line down the picked category +/// and a dot on each series there. +struct LineCrosshair: View { + let x: CGFloat + let plot: CGRect + let dots: [(y: CGFloat, color: Color)] + @Environment(\.openUITheme) private var theme + + var body: some View { + ZStack(alignment: .topLeading) { + Path { path in + path.move(to: CGPoint(x: plot.minX + x, y: plot.minY)) + path.addLine(to: CGPoint(x: plot.minX + x, y: plot.maxY)) + } + .stroke(theme.interactiveBorder, style: StrokeStyle(lineWidth: 1, dash: [4, 4])) + ForEach(Array(dots.enumerated()), id: \.offset) { _, dot in + Circle() + .fill(theme.surface) + .overlay(Circle().strokeBorder(theme.interactiveBorder)) + .overlay(Circle().fill(dot.color).padding(2)) + .frame(width: 8, height: 8) + .position(x: plot.minX + x, y: plot.minY + dot.y) + } + } + } +} diff --git a/packages/swift-lang/Sources/OpenUISwiftUI/Components/ContentComponents.swift b/packages/swift-lang/Sources/OpenUISwiftUI/Components/ContentComponents.swift new file mode 100644 index 0000000000..3275ccbf0d --- /dev/null +++ b/packages/swift-lang/Sources/OpenUISwiftUI/Components/ContentComponents.swift @@ -0,0 +1,793 @@ +import OpenUILang +import SwiftUI + +// MARK: - Card + +struct CardView: View { + let props: ComponentProps + @Environment(\.openUITheme) private var theme + + var body: some View { + let sources = props.array("sources") + VStack(alignment: .leading, spacing: theme.spacing) { + OpenUINodes(props.array("children")) + if !sources.isEmpty { + SourcesStrip(sources: sources) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .surface("card") + .environment(\.openUICardSources, sources) + } +} + +/// Numbered references cited inline as [1], [2] in the card's text. +private struct SourcesStrip: View { + let sources: [OpenUIValue] + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + Text("Sources").font(.subheadline.weight(.medium)).foregroundStyle(.secondary) + ScrollView(.horizontal, showsIndicators: false) { + HStack(alignment: .top, spacing: 12) { + ForEach(sources.indices, id: \.self) { index in + SourceCard(source: sources[index]) + } + } + } + } + } +} + +/// One source, like react-ui's listed source: favicon and site name, then the +/// title. Opens the source when it has a URL. +private struct SourceCard: View { + let source: OpenUIValue + @Environment(\.openUITheme) private var theme + + var body: some View { + let url = source["url"].stringValue.flatMap(URL.init(string:)) + let card = Hovering { hovered in + VStack(alignment: .leading, spacing: 4) { + HStack(spacing: 8) { + Favicon(host: url?.host()) + Text(displayText(source["sourceName"])).font(.caption.weight(.medium)).lineLimit(1) + } + Text(displayText(source["title"])) + .font(.caption) + .foregroundStyle(.secondary) + .lineLimit(2) + .multilineTextAlignment(.leading) + } + .padding(8) + .frame(width: 180, alignment: .topLeading) + .background( + Color.primary.opacity(hovered ? 0.03 : 0), in: RoundedRectangle(cornerRadius: 10) + ) + .overlay( + RoundedRectangle(cornerRadius: 10).strokeBorder( + hovered ? theme.interactiveBorder : theme.border)) + } + if let url { + Link(destination: url) { card }.buttonStyle(.openUIHover) + } else { + card + } + } +} + +/// A site's favicon from Google's favicon service, as react-ui uses, with a +/// globe while it loads or when there's no URL. +private struct Favicon: View { + let host: String? + + var body: some View { + Group { + if let host, let url = URL(string: "https://www.google.com/s2/favicons?sz=128&domain=\(host)") + { + AsyncImage(url: url) { image in + image.resizable().scaledToFit() + } placeholder: { + Image(systemName: "globe").foregroundStyle(.secondary) + } + } else { + Image(systemName: "globe").foregroundStyle(.secondary) + } + } + .frame(width: 20, height: 20) + .clipShape(RoundedRectangle(cornerRadius: 4)) + } +} + +struct CardHeaderView: View { + let props: ComponentProps + + var body: some View { + VStack(alignment: .leading, spacing: 2) { + if let title = props.string("title"), !title.isEmpty { + Text(title).font(.title3.weight(.semibold)) + } + if let subtitle = props.string("subtitle"), !subtitle.isEmpty { + Text(subtitle).font(.subheadline).foregroundStyle(.secondary) + } + } + } +} + +// MARK: - Text + +struct TextContentView: View { + let props: ComponentProps + + var body: some View { + // Full markdown with citations, like react-ui's TextContentWrapper. + MarkdownBlocks(props.text("text"), citations: true) + .font(font) + .frame(maxWidth: .infinity, alignment: .leading) + } + + private var font: Font { + switch props.string("size") { + case "small": return .footnote + case "large": return .title3 + case "small-heavy": return .footnote.weight(.semibold) + case "large-heavy": return .title3.weight(.semibold) + default: return .body + } + } +} + +struct MarkDownRendererView: View { + let props: ComponentProps + + var body: some View { + MarkdownBlocks(props.text("textMarkdown")) + .frame(maxWidth: .infinity, alignment: .leading) + .surface(props.string("variant") ?? "clear") + } +} + +struct InlineHeaderView: View { + let props: ComponentProps + + var body: some View { + VStack(alignment: .leading, spacing: 2) { + Text(props.text("heading")).font(.headline) + if let description = props.string("description"), !description.isEmpty { + Text(description).font(.subheadline).foregroundStyle(.secondary) + } + } + } +} + +// MARK: - Callouts + +struct CalloutView: View { + let props: ComponentProps + @Environment(OpenUIContext.self) private var context + + var body: some View { + let field = context.stateField(name: "visible", binding: props["visible"], form: nil) + let visible = field.isReactive ? (field.value == true || field.value == "true") : true + if visible { + Banner( + variant: props.string("variant"), title: props.text("title"), + description: props.text("description") + ) + .task(id: field.isReactive && !context.isStreaming) { + // A $visible binding auto-dismisses the callout after 3 seconds. + guard field.isReactive, !context.isStreaming else { return } + try? await Task.sleep(nanoseconds: 3_000_000_000) + if !Task.isCancelled { field.setValue(false) } + } + } + } +} + +struct TextCalloutView: View { + let props: ComponentProps + + var body: some View { + Banner( + variant: props.string("variant") ?? "neutral", title: props.text("title"), + description: props.text("description")) + } +} + +private struct Banner: View { + let variant: String? + let title: String + let description: String + @Environment(\.openUITheme) private var theme + + var body: some View { + let color = theme.status(variant) + HStack(alignment: .top, spacing: 10) { + Image(systemName: symbol).foregroundStyle(color) + VStack(alignment: .leading, spacing: 2) { + if !title.isEmpty { Text(title).font(.subheadline.weight(.semibold)) } + if !description.isEmpty { InlineMarkdown(description).font(.subheadline) } + } + Spacer(minLength: 0) + } + .padding(12) + .background(color.opacity(0.1), in: RoundedRectangle(cornerRadius: theme.smallCornerRadius)) + .overlay( + RoundedRectangle(cornerRadius: theme.smallCornerRadius).strokeBorder(color.opacity(0.35))) + } + + private var symbol: String { + switch variant { + case "warning": return "exclamationmark.triangle.fill" + case "error", "danger": return "xmark.octagon.fill" + case "success": return "checkmark.circle.fill" + case "info": return "info.circle.fill" + default: return "text.bubble.fill" + } + } +} + +// MARK: - Code + +/// A code block like react-ui's: Prism's `vscDarkPlus` colors (in light mode +/// too), and a copy button in the corner (on hover on the Mac, as on the web). +struct CodeBlockView: View { + let props: ComponentProps + + var body: some View { + CodeBlockContent( + code: props.text("codeString"), language: props.string("language"), + theme: .darkPlus) + } +} + +/// Highlighted code with a copy button, shared with code in markdown. +struct CodeBlockContent: View { + let code: String + let language: String? + let theme: SyntaxHighlighter.Theme + @State private var copied = false + @State private var hovering = false + @Environment(\.openUITheme) private var openUITheme + + var body: some View { + ScrollView(.horizontal, showsIndicators: false) { + Text(SyntaxHighlighter.highlight(code, language: language, theme: theme)) + .font(.system(size: 13, design: .monospaced)) + .lineSpacing(4) + .textSelection(.enabled) + .fixedSize(horizontal: true, vertical: false) + .padding(13) + } + .frame(maxWidth: .infinity, alignment: .leading) + .background(theme.background, in: RoundedRectangle(cornerRadius: 8)) + .overlay(alignment: .topTrailing) { + copyButton + .padding(8) + .opacity(showsCopyButton ? 1 : 0) + .animation(.easeInOut(duration: 0.2), value: showsCopyButton) + } + .onHover { hovering = $0 } + } + + private var showsCopyButton: Bool { + #if os(macOS) + hovering || copied + #else + true + #endif + } + + private var copyButton: some View { + Button { + copyToPasteboard(code) + copied = true + Task { + try? await Task.sleep(for: .seconds(1)) + copied = false + } + } label: { + Image(systemName: copied ? "checkmark" : "doc.on.doc") + .font(.caption.weight(.semibold)) + .contentTransition(.symbolEffect(.replace)) + .foregroundStyle(copied ? openUITheme.success : Color.primary) + .frame(width: 26, height: 26) + .background( + copied ? openUITheme.success.opacity(0.15) : Color.platformElevatedBackground, + in: RoundedRectangle(cornerRadius: 6) + ) + .overlay(RoundedRectangle(cornerRadius: 6).strokeBorder(Color.primary.opacity(0.12))) + } + .buttonStyle(.plain) + .accessibilityLabel(copied ? "Copied to clipboard" : "Copy code") + } +} + +/// Puts `text` on the system pasteboard. +@MainActor +func copyToPasteboard(_ text: String) { + #if os(macOS) + NSPasteboard.general.clearContents() + NSPasteboard.general.setString(text, forType: .string) + #else + UIPasteboard.general.string = text + #endif +} + +// MARK: - Images + +struct ImageView: View { + let props: ComponentProps + + var body: some View { + RemoteImage(src: props.string("src"), alt: props.text("alt"), fit: true) + } +} + +struct ImageBlockView: View { + let props: ComponentProps + + var body: some View { + RemoteImage(src: props.string("src"), alt: props.text("alt"), fit: true) + } +} + +/// react-ui's ImageGallery: the first five images in a mosaic, a "Show All" +/// button when there are more, and a viewer with every image. Tapping an image +/// opens the viewer on it. +struct ImageGalleryView: View { + let props: ComponentProps + @State private var selected = 0 + @State private var showsViewer = false + @Environment(\.openUITheme) private var theme + + var body: some View { + let images = props.array("images") + if !images.isEmpty { + GalleryMosaic { + ForEach(Array(images.prefix(GalleryMosaic.maxImages).enumerated()), id: \.offset) { + index, image in + Button { + selected = index + showsViewer = true + } label: { + GalleryTile(src: image["src"].stringValue) + } + .buttonStyle(.plain) + .accessibilityLabel(galleryImageLabel(image, index)) + } + } + .clipShape(RoundedRectangle(cornerRadius: theme.cornerRadius)) + .overlay(alignment: .bottomTrailing) { + if images.count > GalleryMosaic.maxImages { + // Filled by hand: over a photo, a bordered button that loses its + // fill in an inactive Mac window can't be read. + Button { + showsViewer = true + } label: { + Text("Show All") + .font(.footnote.weight(.medium)) + .foregroundStyle(theme.onAccent) + .padding(.horizontal, 10) + .padding(.vertical, 5) + .background(theme.accent, in: RoundedRectangle(cornerRadius: theme.smallCornerRadius)) + } + .buttonStyle(.plain) + .padding(5) + } + } + .sheet(isPresented: $showsViewer) { + GalleryViewer(images: images, selected: $selected) + .environment(\.openUITheme, theme) + } + } + } +} + +private func galleryImageLabel(_ image: OpenUIValue, _ index: Int) -> String { + let alt = displayText(image["alt"]) + return alt.isEmpty ? "Gallery image \(index + 1)" : alt +} + +/// A mosaic image that zooms in a little under the pointer, as react-ui's do. +private struct GalleryTile: View { + let src: String? + @State private var hovering = false + + var body: some View { + RemoteImage(src: src, alt: "", rounded: false) + .scaleEffect(hovering ? 1.05 : 1) + .clipped() + .contentShape(Rectangle()) + .onHover { hovering in + withOpenUIAnimation(.easeOut(duration: 0.3)) { self.hovering = hovering } + } + } +} + +/// Lays out up to five gallery images like react-ui's grid templates: one or +/// two side by side, otherwise the first image large and the rest in two rows +/// beside it (or, when narrow, in rows under it). +struct GalleryMosaic: Layout { + static let maxImages = 5 + /// react-ui caps the grid at 376px. + static let maxHeight: CGFloat = 376 + /// react-ui's `space-s`. + static let gap: CGFloat = 8 + /// react-ui switches to its narrow templates under a 768px viewport. A chat + /// column is much narrower than the window it is in, so this goes by the + /// gallery's own width. + static let narrowWidth: CGFloat = 520 + + /// A CSS grid template: column weights (`fr`), a row count, and each image's + /// column, row and spans. + struct Template: Equatable { + var columns: [CGFloat] + var rows: Int + var cells: [Cell] + + struct Cell: Equatable { + var column: Int + var row: Int + var columnSpan = 1 + var rowSpan = 1 + } + } + + static func template(count: Int, narrow: Bool) -> Template { + typealias Cell = Template.Cell + switch min(count, maxImages) { + case 0: return Template(columns: [1], rows: 1, cells: []) + case 1: return Template(columns: [1], rows: 1, cells: [Cell(column: 0, row: 0)]) + case 2: + return Template( + columns: [1, 1], rows: 1, cells: [Cell(column: 0, row: 0), Cell(column: 1, row: 0)]) + case 3 where narrow: + return Template( + columns: [1, 1], rows: 2, + cells: [ + Cell(column: 0, row: 0, columnSpan: 2), Cell(column: 0, row: 1), Cell(column: 1, row: 1), + ]) + case 3: + return Template( + columns: [1, 1], rows: 2, + cells: [ + Cell(column: 0, row: 0, rowSpan: 2), Cell(column: 1, row: 0), Cell(column: 1, row: 1), + ]) + case 4: + return Template( + columns: [1, 1, 1, 1], rows: 2, + cells: [ + Cell(column: 0, row: 0, columnSpan: 2, rowSpan: 2), + Cell(column: 2, row: 0, columnSpan: 2), + Cell(column: 2, row: 1), Cell(column: 3, row: 1), + ]) + case _ where narrow: + return Template( + columns: Array(repeating: 1, count: 6), rows: 2, + cells: [ + Cell(column: 0, row: 0, columnSpan: 3), Cell(column: 3, row: 0, columnSpan: 3), + Cell(column: 0, row: 1, columnSpan: 2), Cell(column: 2, row: 1, columnSpan: 2), + Cell(column: 4, row: 1, columnSpan: 2), + ]) + default: + return Template( + columns: [2, 1, 1], rows: 2, + cells: [ + Cell(column: 0, row: 0, rowSpan: 2), Cell(column: 1, row: 0), Cell(column: 2, row: 0), + Cell(column: 1, row: 1), Cell(column: 2, row: 1), + ]) + } + } + + /// react-ui's grid takes its height from the images, up to 376px. That + /// isn't known until they load, so this uses a fixed shape instead: no jump + /// when they arrive. + static func height(width: CGFloat, count: Int) -> CGFloat { + let ratio: CGFloat = + switch count { + case 1: 0.6 + case 2: 0.45 + default: width < narrowWidth ? 0.8 : 0.55 + } + return min(maxHeight, (width * ratio).rounded()) + } + + /// Each image's frame, in order. + static func frames(count: Int, in bounds: CGRect) -> [CGRect] { + let template = template(count: count, narrow: bounds.width < narrowWidth) + let unit = + (bounds.width - gap * CGFloat(template.columns.count - 1)) / template.columns.reduce(0, +) + let rowHeight = (bounds.height - gap * CGFloat(template.rows - 1)) / CGFloat(template.rows) + func x(_ column: Int) -> CGFloat { + bounds.minX + template.columns.prefix(column).reduce(0, +) * unit + gap * CGFloat(column) + } + return template.cells.map { cell in + let columns = template.columns[cell.column.. CGSize { + // Without a usable width (an unbounded stack), fall back to a phone's. + let width = proposal.width.flatMap { $0.isFinite ? $0 : nil } ?? 360 + return CGSize(width: width, height: Self.height(width: width, count: subviews.count)) + } + + func placeSubviews( + in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout () + ) { + for (subview, frame) in zip(subviews, Self.frames(count: subviews.count, in: bounds)) { + subview.place(at: frame.origin, proposal: ProposedViewSize(frame.size)) + } + } +} + +/// react-ui's gallery modal: every image, one at a time, over a strip of +/// thumbnails. Swipe or use the arrow keys to move between them. +struct GalleryViewer: View { + let images: [OpenUIValue] + @Binding var selected: Int + @State private var page: Int? + @FocusState private var focused: Bool + @Environment(\.dismiss) private var dismiss + @Environment(\.openUITheme) private var theme + + init(images: [OpenUIValue], selected: Binding) { + self.images = images + _selected = selected + _page = State(initialValue: selected.wrappedValue) + } + + var body: some View { + VStack(spacing: 24) { + HStack { + Text("All Photos").font(.headline) + Spacer() + Button { + dismiss() + } label: { + Image(systemName: "xmark") + .font(.footnote.weight(.semibold)) + .frame(width: 28, height: 28) + .background( + theme.sunkSurface, in: RoundedRectangle(cornerRadius: theme.smallCornerRadius)) + } + .buttonStyle(.plain) + .keyboardShortcut(.cancelAction) + .accessibilityLabel("Close gallery") + } + .padding(.horizontal, 24) + pages + thumbnails + } + .padding(.vertical, 24) + #if os(macOS) + .frame(minWidth: 560, idealWidth: 720, minHeight: 520, idealHeight: 640) + #endif + .focusable() + .focusEffectDisabled() + .focused($focused) + .onAppear { focused = true } + .onKeyPress(.leftArrow) { show(selected - 1) } + .onKeyPress(.rightArrow) { show(selected + 1) } + .onChange(of: page) { _, page in + if let page { selected = page } + } + } + + /// The images full size, one page each. Pages have a fixed size, so the + /// lazy stack never has to re-measure anything while it scrolls. + private var pages: some View { + ScrollView(.horizontal) { + LazyHStack(spacing: 0) { + ForEach(images.indices, id: \.self) { index in + let details = displayText(images[index]["details"]) + VStack(spacing: 8) { + RemoteImage( + src: images[index]["src"].stringValue, alt: galleryImageLabel(images[index], index), + fit: true + ) + .frame(maxHeight: .infinity) + if !details.isEmpty { + Text(details).font(.footnote).foregroundStyle(.secondary) + .multilineTextAlignment(.center) + } + } + .padding(.horizontal, 24) + .containerRelativeFrame([.horizontal, .vertical]) + .id(index) + } + } + .scrollTargetLayout() + } + .scrollTargetBehavior(.paging) + .scrollPosition(id: $page) + .scrollIndicators(.never) + } + + private var thumbnails: some View { + ScrollViewReader { proxy in + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: GalleryMosaic.gap) { + ForEach(images.indices, id: \.self) { index in + let isSelected = index == selected + Button { + page = index + } label: { + RemoteImage(src: images[index]["src"].stringValue, alt: "") + .frame(height: 116) + .containerRelativeFrame(.horizontal) { width, _ in + // Thumbnails share the row, but are at least 174 wide. + let count = CGFloat(images.count) + return max(174, (width - 48 - GalleryMosaic.gap * (count - 1)) / count) + } + .opacity(isSelected ? 1 : 0.6) + .overlay( + RoundedRectangle(cornerRadius: theme.smallCornerRadius) + .strokeBorder(isSelected ? theme.accent : .clear)) + } + .buttonStyle(.plain) + .accessibilityLabel(galleryImageLabel(images[index], index)) + .accessibilityAddTraits(isSelected ? .isSelected : []) + .id(index) + } + } + .padding(.horizontal, 24) + } + .onAppear { proxy.scrollTo(selected, anchor: .center) } + .onChange(of: selected) { _, selected in + withOpenUIAnimation(Motion.reveal) { proxy.scrollTo(selected, anchor: .center) } + } + } + } + + private func show(_ index: Int) -> KeyPress.Result { + guard images.indices.contains(index) else { return .ignored } + page = index + return .handled + } +} + +/// An image loaded from a URL, with the alt text as its accessibility label +/// and a placeholder while loading (or when the URL is missing). +/// +/// `fit` keeps the image's aspect ratio at the available width (content +/// images). Otherwise the image fills whatever frame the caller gives it and +/// never affects layout, so a large photo can't widen a card or grid column. +struct RemoteImage: View { + let src: String? + let alt: String + var fit = false + /// Off for images inside something that clips them, like the gallery mosaic. + var rounded = true + @Environment(\.openUITheme) private var theme + + var body: some View { + Group { + if fit { + AsyncImage(url: url, transaction: Self.fadeIn) { phase in + if let image = phase.image { + image.resizable().scaledToFit().transition(.opacity) + } else { + placeholder(phase).frame(height: 160) + } + } + .frame(maxWidth: .infinity) + } else { + Color.clear + .overlay { + AsyncImage(url: url, transaction: Self.fadeIn) { phase in + if let image = phase.image { + image.resizable().scaledToFill().transition(.opacity) + } else { + placeholder(phase) + } + } + } + .clipped() + } + } + .clipShape(RoundedRectangle(cornerRadius: rounded ? theme.smallCornerRadius : 0)) + .accessibilityLabel(alt) + } + + private var url: URL? { src.flatMap(URL.init(string:)) } + + /// The photo fades in over its loading placeholder. + private static let fadeIn = Transaction(animation: .easeOut(duration: 0.25)) + + @ViewBuilder + private func placeholder(_ phase: AsyncImagePhase) -> some View { + if phase.error == nil, url != nil { + // Still loading: pulse like react-ui's skeletons. + GeometryReader { size in SkeletonBlock(height: size.size.height, cornerRadius: 0) } + } else { + ZStack { + theme.sunkSurface + Image(systemName: "photo.badge.exclamationmark").font(.title2).foregroundStyle(.secondary) + } + } + } +} + +// MARK: - Separator, tags and key/value rows + +struct SeparatorView: View { + let props: ComponentProps + + var body: some View { + if props.string("orientation") == "vertical" { + Divider().frame(maxHeight: 24) + } else { + Divider() + } + } +} + +struct TagBlockView: View { + let props: ComponentProps + @Environment(\.openUITheme) private var theme + + var body: some View { + FlowLayout(spacing: theme.compactSpacing) { + ForEach(Array(props.array("tags").enumerated()), id: \.offset) { _, tag in + let size = props.string("size") + // Tags, like react-ui's: one line, cut short when wider than the block. + Text(displayText(tag)) + .lineLimit(1) + .font(font) + .padding(tagPadding(size)) + .background(theme.sunkSurface, in: RoundedRectangle(cornerRadius: tagRadius(size))) + } + } + } + + private var font: Font { + switch props.string("size") { + case "sm": return .caption2 + case "lg": return .callout + default: return .caption + } + } +} + +struct EntityListView: View { + let props: ComponentProps + @Environment(\.openUITheme) private var theme + + var body: some View { + let small = props.string("size") == "small" + VStack(spacing: 0) { + if !small, let header = props["header"].objectValue { + row(.object(header), emphasis: true) + Divider() + } + ForEach(Array(props.array("rows").enumerated()), id: \.offset) { index, item in + if index > 0 { Divider().opacity(0.5) } + row(item, emphasis: false) + } + if !small, let footer = props["footer"].objectValue { + Divider() + row(.object(footer), emphasis: true) + } + } + .font(small ? .footnote : .callout) + } + + private func row(_ item: OpenUIValue, emphasis: Bool) -> some View { + HStack { + Text(displayText(item["left"])).foregroundStyle(emphasis ? .primary : .secondary) + Spacer(minLength: theme.spacing) + Text(displayText(item["right"])) + .monospacedDigit() + .multilineTextAlignment(.trailing) + } + .fontWeight(emphasis ? .semibold : .regular) + .padding(.vertical, 6) + } +} diff --git a/packages/swift-lang/Sources/OpenUISwiftUI/Components/FormComponents.swift b/packages/swift-lang/Sources/OpenUISwiftUI/Components/FormComponents.swift new file mode 100644 index 0000000000..99758e8928 --- /dev/null +++ b/packages/swift-lang/Sources/OpenUISwiftUI/Components/FormComponents.swift @@ -0,0 +1,912 @@ +import OpenUILang +import SwiftUI + +// MARK: - Form + +struct FormView: View { + let props: ComponentProps + @State private var validation = FormValidation() + @Environment(\.openUITheme) private var theme + + var body: some View { + VStack(alignment: .leading, spacing: theme.spacing + 4) { + OpenUINode(props["fields"]) + OpenUINode(props["buttons"]) + } + .environment(\.openUIFormName, props.string("name")) + .environment(validation) + } +} + +struct FormControlView: View { + let props: ComponentProps + @Environment(FormValidation.self) private var validation: FormValidation? + @Environment(\.openUITheme) private var theme + + var body: some View { + let input = props.children("input").first + let fieldName = input?.string("name") + let required = input?["rules"]["required"] == true + VStack(alignment: .leading, spacing: 6) { + Text(props.text("label") + (required ? "*" : "")).font(.subheadline.weight(.medium)) + OpenUINode(props["input"]) + if let fieldName, let error = validation?.error(for: fieldName) { + Label(error, systemImage: "exclamationmark.circle").font(.caption).foregroundStyle( + theme.danger) + } else if let hint = props.string("hint"), !hint.isEmpty { + Text(hint).font(.caption).foregroundStyle(.secondary) + } + } + } +} + +struct LabelView: View { + let props: ComponentProps + + var body: some View { + Text(props.text("text")).font(.subheadline.weight(.medium)) + } +} + +/// The field a form component reads and writes, plus its validation rules. +private struct FieldContext { + let field: StateField + let rules: [ParsedRule] + + @MainActor + init(_ props: ComponentProps, context: OpenUIContext, form: String?) { + self.field = context.stateField(name: props.text("name"), binding: props["value"], form: form) + self.rules = parseStructuredRules(props["rules"]) + } +} + +// MARK: - Text input + +struct InputView: View { + let props: ComponentProps + @Environment(OpenUIContext.self) private var context + @Environment(\.openUIFormName) private var form + @Environment(FormValidation.self) private var validation: FormValidation? + + var body: some View { + let state = FieldContext(props, context: context, form: form) + let text = Binding( + get: { displayText(state.field.value) }, + set: { newValue in + state.field.setValue(.string(newValue)) + if !state.rules.isEmpty { validation?.clearError(state.field.name) } + }) + Group { + if props.string("type") == "password" { + SecureField(props.text("placeholder"), text: text) + } else { + TextField(props.text("placeholder"), text: text) + #if os(iOS) + .keyboardType(keyboard) + .textInputAutocapitalization(autocapitalization) + #endif + } + } + .textFieldStyle(.plain) + .fieldSurface() + .disabled(context.isStreaming) + .onSubmit { + if !state.rules.isEmpty { + validation?.validateField(state.field.name, value: state.field.value, rules: state.rules) + } + } + .formField(state.field.name, rules: state.rules, value: state.field.value) + } + + #if os(iOS) + private var keyboard: UIKeyboardType { + switch props.string("type") { + case "email": return .emailAddress + case "url": return .URL + case "number": return .decimalPad + default: return .default + } + } + + private var autocapitalization: TextInputAutocapitalization { + ["email", "url"].contains(props.string("type") ?? "") ? .never : .sentences + } + #endif +} + +struct TextAreaView: View { + let props: ComponentProps + @Environment(OpenUIContext.self) private var context + @Environment(\.openUIFormName) private var form + @Environment(FormValidation.self) private var validation: FormValidation? + + var body: some View { + let state = FieldContext(props, context: context, form: form) + // At most a screenful: the count comes from the response. + let rows = Int(min(max(props.number("rows").flatMap(\.finite) ?? 3, 1), 20)) + TextField( + props.text("placeholder"), + text: Binding( + get: { displayText(state.field.value) }, + set: { newValue in + state.field.setValue(.string(newValue)) + if !state.rules.isEmpty { validation?.clearError(state.field.name) } + }), + axis: .vertical + ) + .lineLimit(rows...max(rows, 12)) + .textFieldStyle(.plain) + .fieldSurface() + .disabled(context.isStreaming) + .formField(state.field.name, rules: state.rules, value: state.field.value) + } +} + +// MARK: - Choices + +struct SelectView: View { + let props: ComponentProps + @Environment(\.openUITheme) private var theme + @Environment(OpenUIContext.self) private var context + @Environment(\.openUIFormName) private var form + @Environment(FormValidation.self) private var validation: FormValidation? + + var body: some View { + let state = FieldContext(props, context: context, form: form) + let items = props.children("items") + let current = displayText(state.field.value) + let label = items.first { $0.text("value") == current }?.text("label") + // A full-width field like react-ui's select, opening a native menu. + Menu { + Picker( + props.text("placeholder"), + selection: Binding( + get: { current }, + set: { newValue in + state.field.setValue(.string(newValue)) + if !state.rules.isEmpty { + validation?.validateField( + state.field.name, value: .string(newValue), rules: state.rules) + } + }) + ) { + ForEach(items.indices, id: \.self) { index in + Text(items[index].text("label")).tag(items[index].text("value")) + } + } + .pickerStyle(.inline) + } label: { + HStack { + Text(label ?? props.string("placeholder") ?? "Select…") + .foregroundStyle(label == nil ? .secondary : .primary) + Spacer() + Image(systemName: "chevron.down").font(.caption).foregroundStyle(.secondary) + } + .frame(maxWidth: .infinity) + .fieldSurface(border: theme.border) + .contentShape(Rectangle()) + } + .menuIndicator(.hidden) + .buttonStyle(.plain) + .disabled(context.isStreaming) + .formField(state.field.name, rules: state.rules, value: state.field.value) + } +} + +struct SelectItemView: View { + let props: ComponentProps + + var body: some View { + Text(props.text("label")) + } +} + +struct RadioGroupView: View { + let props: ComponentProps + @Environment(OpenUIContext.self) private var context + @Environment(\.openUIFormName) private var form + @Environment(FormValidation.self) private var validation: FormValidation? + + var body: some View { + let state = FieldContext(props, context: context, form: form) + let selected = + state.field.value.isNullish ? props.text("defaultValue") : displayText(state.field.value) + VStack(alignment: .leading, spacing: 8) { + ForEach(Array(props.children("items").enumerated()), id: \.offset) { _, item in + let value = item.text("value") + Button { + state.field.setValue(.string(value)) + if !state.rules.isEmpty { + validation?.validateField(state.field.name, value: .string(value), rules: state.rules) + } + } label: { + OptionRow( + symbol: value == selected ? "largecircle.fill.circle" : "circle", + label: item.text("label"), description: item.text("description")) + } + .buttonStyle(.openUIHover) + } + } + .disabled(context.isStreaming) + .formField(state.field.name, rules: state.rules, value: state.field.value) + } +} + +struct RadioItemView: View { + let props: ComponentProps + + var body: some View { + OptionRow(symbol: "circle", label: props.text("label"), description: props.text("description")) + } +} + +/// A group of named booleans stored as one `{ name: Bool }` record, shared by +/// check boxes and switches. Unset items fall back to `defaultChecked`. +private struct BooleanGroup: View { + let props: ComponentProps + let style: Style + @Environment(OpenUIContext.self) private var context + @Environment(\.openUIFormName) private var form + @Environment(FormValidation.self) private var validation: FormValidation? + + enum Style { case checkbox, toggle } + + var body: some View { + let state = FieldContext(props, context: context, form: form) + let items = props.children("items") + let stored = state.field.value.objectValue ?? OpenUIObject() + let aggregate = OpenUIObject( + items.map { item in + let name = item.text("name") + let value = stored[name]?.boolValue ?? item.bool("defaultChecked") ?? false + return (name, OpenUIValue.bool(value)) + }) + VStack(alignment: .leading, spacing: 8) { + ForEach(Array(items.enumerated()), id: \.offset) { _, item in + let name = item.text("name") + let isOn = aggregate[name]?.boolValue ?? false + let toggle = { + var next = aggregate + next[name] = .bool(!isOn) + state.field.setValue(.object(next)) + if !state.rules.isEmpty { + validation?.validateField(state.field.name, value: .object(next), rules: state.rules) + } + } + switch style { + case .checkbox: + Button(action: toggle) { + OptionRow( + symbol: isOn ? "checkmark.square.fill" : "square", label: item.text("label"), + description: item.text("description")) + } + .buttonStyle(.openUIHover) + case .toggle: + // The switch leads and the text follows, as in react-ui's SwitchItem. + HStack(alignment: .top, spacing: 8) { + Toggle(item.text("label"), isOn: Binding(get: { isOn }, set: { _ in toggle() })) + .toggleStyle(.switch) + .labelsHidden() + VStack(alignment: .leading, spacing: 2) { + Text(item.text("label")) + if let description = item.string("description"), !description.isEmpty { + Text(description).font(.caption).foregroundStyle(.secondary) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + } + } + } + } + .disabled(context.isStreaming) + .formField(state.field.name, rules: state.rules, value: state.field.value) + } +} + +struct CheckBoxGroupView: View { + let props: ComponentProps + var body: some View { BooleanGroup(props: props, style: .checkbox) } +} + +struct SwitchGroupView: View { + let props: ComponentProps + var body: some View { + BooleanGroup(props: props, style: .toggle).surface(props.string("variant") ?? "clear") + } +} + +struct CheckBoxItemView: View { + let props: ComponentProps + var body: some View { + OptionRow( + symbol: props.bool("defaultChecked") == true ? "checkmark.square.fill" : "square", + label: props.text("label"), description: props.text("description")) + } +} + +struct SwitchItemView: View { + let props: ComponentProps + var body: some View { + Toggle(props.text("label"), isOn: .constant(props.bool("defaultChecked") ?? false)) + .toggleStyle(.switch) + .disabled(true) + } +} + +/// A selectable row: indicator symbol, label and optional description. +private struct OptionRow: View { + let symbol: String + let label: String + let description: String + @Environment(\.openUITheme) private var theme + @Environment(\.openUIHovered) private var hovered + + var body: some View { + let radio = symbol.contains("circle") + let empty = symbol == "circle" || symbol == "square" + HStack(alignment: .firstTextBaseline, spacing: 8) { + Image(systemName: symbol) + .foregroundStyle(theme.accent) + // react-ui fills an empty radio or check box under the pointer. + .background { + if hovered && empty { + RoundedRectangle(cornerRadius: radio ? 10 : 3) + .fill(radio ? theme.interactiveBorder : theme.sunkSurface) + .padding(2) + } + } + VStack(alignment: .leading, spacing: 2) { + Text(label) + if !description.isEmpty { + Text(description).font(.caption).foregroundStyle(.secondary) + } + } + Spacer(minLength: 0) + } + .contentShape(Rectangle()) + } +} + +// MARK: - Date and slider + +/// Dates are stored as `yyyy-MM-dd` strings; a range as `{ from, to }`. +private let dayFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.calendar = Calendar(identifier: .gregorian) + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.dateFormat = "yyyy-MM-dd" + return formatter +}() + +struct DatePickerView: View { + let props: ComponentProps + @Environment(\.openUITheme) private var theme + @Environment(OpenUIContext.self) private var context + @Environment(\.openUIFormName) private var form + @Environment(FormValidation.self) private var validation: FormValidation? + + var body: some View { + let state = FieldContext(props, context: context, form: form) + let isRange = props.string("mode") == "range" + if state.field.value.isNullish { + // Nothing is picked until the user picks it, so the form can't submit a + // date the user never chose. + Button { + let today = OpenUIValue.string(dayFormatter.string(from: Date())) + set(state, isRange ? .object(["from": today, "to": today]) : today) + } label: { + HStack { + Text(isRange ? "Select a range" : "Select a date").foregroundStyle(.secondary) + Spacer() + Image(systemName: "calendar").foregroundStyle(.secondary) + } + .fieldSurface(border: theme.border) + } + .buttonStyle(.plain) + .disabled(context.isStreaming) + .formField(state.field.name, rules: state.rules, value: state.field.value) + } else if isRange { + VStack(alignment: .leading, spacing: 6) { + rangePicker("From", key: "from", state) + rangePicker("To", key: "to", state) + } + .formField(state.field.name, rules: state.rules, value: state.field.value) + } else { + DatePicker( + "", + selection: Binding( + get: { state.field.value.stringValue.flatMap(dayFormatter.date(from:)) ?? Date() }, + set: { set(state, .string(dayFormatter.string(from: $0))) }), + displayedComponents: .date + ) + .labelsHidden() + .disabled(context.isStreaming) + .formField(state.field.name, rules: state.rules, value: state.field.value) + } + } + + private func rangePicker(_ label: String, key: String, _ state: FieldContext) -> some View { + let range = state.field.value.objectValue ?? OpenUIObject() + let day = { (key: String) in range[key]?.stringValue.flatMap(dayFormatter.date(from:)) } + let selection = Binding( + get: { day(key) ?? Date() }, + set: { (date: Date) in + var next = range + next[key] = .string(dayFormatter.string(from: date)) + set(state, .object(next)) + }) + // The ends can't cross, as in react-ui's range calendar. + return Group { + if key == "from", let to = day("to") { + DatePicker(label, selection: selection, in: ...to, displayedComponents: .date) + } else if key == "to", let from = day("from") { + DatePicker(label, selection: selection, in: from..., displayedComponents: .date) + } else { + DatePicker(label, selection: selection, displayedComponents: .date) + } + } + .disabled(context.isStreaming) + } + + private func set(_ state: FieldContext, _ value: OpenUIValue) { + state.field.setValue(value) + if !state.rules.isEmpty { + validation?.validateField(state.field.name, value: value, rules: state.rules) + } + } +} + +/// A slider like react-ui's SliderBlock: the label with editable values (a +/// menu of steps when discrete), then the track. Two values make it a range +/// with a thumb at each end. +struct SliderView: View { + let props: ComponentProps + @Environment(OpenUIContext.self) private var context + @Environment(\.openUIFormName) private var form + @Environment(FormValidation.self) private var validation: FormValidation? + @Environment(\.openUITheme) private var theme + + var body: some View { + let state = FieldContext(props, context: context, form: form) + // The bounds come from the response; one that isn't a finite number + // (1e999, 0/0) falls back to react-ui's default rather than breaking the range. + let minimum = props.number("min").flatMap(\.finite) ?? 0 + let maximum = max(props.number("max").flatMap(\.finite) ?? 100, minimum) + let discrete = props.string("variant") == "discrete" + // react-ui steps a continuous slider by at least 1. + let step = + props.number("step").flatMap(\.finite).map { discrete ? max($0, 0.0001) : max($0, 1) } ?? 1 + let stored = state.field.value.isNullish ? props["defaultValue"] : state.field.value + let values = (stored.arrayValue ?? []).compactMap(\.numberValue) + let current = values.isEmpty ? [minimum] : Array(values.prefix(2)) + let errors = discrete ? [] : Self.errors(current, minimum: minimum, maximum: maximum) + let set: @MainActor ([Double]) -> Void = { next in + state.field.setValue(.array(next.map { .number($0) })) + if !state.rules.isEmpty { + validation?.validateField(state.field.name, value: .number(next[0]), rules: state.rules) + } + } + VStack(alignment: .leading, spacing: 6) { + HStack(spacing: 8) { + Text(props.string("label") ?? state.field.name).font(.subheadline) + Spacer(minLength: 8) + // Step-derived controls wait until the props stop streaming. + if !context.isStreaming { + SliderValueControls( + values: current, minimum: minimum, maximum: maximum, step: step, discrete: discrete, + errors: errors, onChange: set) + } + } + if !context.isStreaming, let error = errors.first(where: { !$0.isEmpty }) { + Label(error, systemImage: "exclamationmark.circle") + .font(.caption).foregroundStyle(theme.danger) + } + if current.count > 1 { + RangeSlider( + lower: RangeSlider.clamp(min(current[0], current[1]), to: minimum...maximum), + upper: RangeSlider.clamp(max(current[0], current[1]), to: minimum...maximum), + bounds: minimum...maximum, step: step + ) { set([$0, $1]) } + } else if maximum - minimum < step { + // SwiftUI's Slider traps unless its range holds a whole step. Mid-stream + // `max` can be cut short below `min` ("8000" arrives as "800"), and a + // response can give a step wider than the range, so the track stays + // idle until there's a range to move in. + Slider(value: .constant(0), in: 0...1).disabled(true) + } else if (maximum - minimum) / step <= 100 { + Slider( + value: Binding( + get: { RangeSlider.clamp(current[0], to: minimum...maximum) }, + set: { set([$0]) }), + in: minimum...maximum, step: step) + } else { + // On the Mac a stepped Slider draws a tick mark per step, and AppKit + // never finishes laying out millions of them; the value snaps here. + Slider( + value: Binding( + get: { RangeSlider.clamp(current[0], to: minimum...maximum) }, + set: { set([RangeSlider.snap($0, bounds: minimum...maximum, step: step)]) }), + in: minimum...maximum) + } + HStack { + Text(compactNumber(minimum)) + Spacer() + Text(compactNumber(maximum)) + } + .font(.caption.monospacedDigit()) + .foregroundStyle(.secondary) + } + .disabled(context.isStreaming) + .formField(state.field.name, rules: state.rules, value: state.field.value) + } + + /// react-ui's messages for typed values: one per value, empty when fine. + static func errors(_ values: [Double], minimum: Double, maximum: Double) -> [String] { + var errors = values.map { value -> String in + if value.isNaN { return "Invalid number" } + if value < minimum || value > maximum { + return "Value must be between \(jsNumberToString(minimum)) and \(jsNumberToString(maximum))" + } + return "" + } + if values.count > 1, values[0] > values[1] { errors[0] = "Min must be less than max" } + return errors + } +} + +/// The values beside a slider's label: number fields, or menus of the steps +/// when the slider is discrete. +struct SliderValueControls: View { + let values: [Double] + let minimum: Double + let maximum: Double + let step: Double + let discrete: Bool + let errors: [String] + let onChange: @MainActor ([Double]) -> Void + + /// Above this many steps a menu is unwieldy, so discrete sliders get a + /// number field instead. + private static let maximumMenuOptions = 200 + + var body: some View { + let options = + discrete ? Self.options(minimum, maximum, step, limit: Self.maximumMenuOptions) : nil + HStack(spacing: 6) { + if values.count > 1 { + let lower = values[0] + let upper = values[1] + if let options { + menu(lower, options.filter { $0 < upper }) { onChange([$0, upper].sorted()) } + separator + menu(upper, options.filter { $0 > lower }) { onChange([lower, $0].sorted()) } + } else { + SliderValueField(value: lower, hasError: !errors[0].isEmpty) { onChange([$0, upper]) } + separator + SliderValueField(value: upper, hasError: !(errors.last ?? "").isEmpty) { + onChange([lower, $0]) + } + } + } else if let options { + menu(values[0], options) { onChange([$0]) } + } else { + SliderValueField(value: values[0], hasError: !(errors.first ?? "").isEmpty) { + onChange([$0]) + } + } + } + } + + private var separator: some View { + Rectangle().fill(Color.secondary.opacity(0.5)).frame(width: 8, height: 1) + } + + private func menu( + _ value: Double, _ options: [Double], _ select: @escaping @MainActor (Double) -> Void + ) + -> some View + { + Picker( + "Value", + selection: Binding(get: { value }, set: { select($0) }) + ) { + ForEach(options, id: \.self) { Text(jsNumberToString($0)).tag($0) } + } + .labelsHidden() + .pickerStyle(.menu) + .fixedSize() + } + + /// Every step from `minimum` to `maximum`, like react-ui's option list, or + /// nil when there are more than `limit` of them. + static func options(_ minimum: Double, _ maximum: Double, _ step: Double, limit: Int) -> [Double]? + { + let steps = ((maximum - minimum) / step).rounded(.down) + guard steps.isFinite, steps >= 0, steps < Double(limit) else { return steps < 0 ? [] : nil } + return (0...Int(steps)).map { minimum + Double($0) * step } + } +} + +/// A small number field that applies valid numbers as they're typed and shows +/// the slider's value again when it loses focus. +private struct SliderValueField: View { + let value: Double + let hasError: Bool + let onChange: @MainActor (Double) -> Void + @State private var text = "" + @FocusState private var focused: Bool + @Environment(\.openUITheme) private var theme + + var body: some View { + TextField("Value", text: $text) + .labelsHidden() + .multilineTextAlignment(.center) + .font(.subheadline.monospacedDigit()) + .frame(width: 64) + .padding(.vertical, 4) + .background(Color.primary.opacity(0.05), in: RoundedRectangle(cornerRadius: 6)) + .overlay( + RoundedRectangle(cornerRadius: 6) + .strokeBorder(hasError ? theme.danger : Color.primary.opacity(0.1)) + ) + .focused($focused) + #if os(iOS) + .keyboardType(.numbersAndPunctuation) + #endif + .onAppear { text = jsNumberToString(value) } + .onChange(of: value) { if !focused { text = jsNumberToString(value) } } + .onChange(of: focused) { if !focused { text = jsNumberToString(value) } } + .onChange(of: text) { + let number = jsStringToNumber(text) + if !number.isNaN, focused { onChange(number) } + } + } +} + +/// One track with a thumb at each end of a range, like react-ui's range +/// slider. A thumb stops at the other one rather than crossing it. +struct RangeSlider: View { + let lower: Double + let upper: Double + let bounds: ClosedRange + let step: Double + let onChange: @MainActor (Double, Double) -> Void + @Environment(\.isEnabled) private var isEnabled + /// Where each thumb (keyed by whether it's the lower one) was when its drag + /// began. A drag's translation is from there, not from where the thumb has + /// moved to since. + @State private var dragStarts: [Bool: CGFloat] = [:] + private let thumbSize: CGFloat = 24 + + var body: some View { + GeometryReader { geometry in + let track = max(geometry.size.width - thumbSize, 1) + ZStack(alignment: .leading) { + Capsule() + .fill(Color.primary.opacity(0.12)) + .frame(height: 4) + .padding(.horizontal, thumbSize / 2) + Capsule() + .fill(.tint) + .frame(width: max(0, offset(upper, track) - offset(lower, track)), height: 4) + .offset(x: offset(lower, track) + thumbSize / 2) + thumb(isLower: true, value: lower, track: track) + thumb(isLower: false, value: upper, track: track) + } + .frame(maxHeight: .infinity) + } + .frame(height: thumbSize + 4) + .opacity(isEnabled ? 1 : 0.5) + } + + private func offset(_ value: Double, _ track: CGFloat) -> CGFloat { + let span = bounds.upperBound - bounds.lowerBound + return span > 0 ? CGFloat((value - bounds.lowerBound) / span) * track : 0 + } + + private func thumb(isLower: Bool, value: Double, track: CGFloat) -> some View { + Circle() + .fill(.white) + .shadow(color: .black.opacity(0.2), radius: 2, y: 1) + .overlay(Circle().strokeBorder(Color.black.opacity(0.06))) + .frame(width: thumbSize, height: thumbSize) + .offset(x: offset(value, track)) + .gesture( + DragGesture(minimumDistance: 0) + .onChanged { drag in + let start = dragStarts[isLower] ?? offset(value, track) + dragStarts[isLower] = start + move( + isLower: isLower, + to: Self.value(at: start + drag.translation.width, track: track, bounds: bounds)) + } + .onEnded { _ in dragStarts[isLower] = nil } + ) + .accessibilityElement() + .accessibilityLabel(isLower ? "Minimum" : "Maximum") + .accessibilityValue(jsNumberToString(value)) + .accessibilityAdjustableAction { direction in + let delta = direction == .increment ? step : -step + move(isLower: isLower, to: value + delta) + } + .focusable() + .onKeyPress(.leftArrow) { + move(isLower: isLower, to: value - step) + return .handled + } + .onKeyPress(.rightArrow) { + move(isLower: isLower, to: value + step) + return .handled + } + } + + private func move(isLower: Bool, to value: Double) { + let snapped = Self.snap(value, bounds: bounds, step: step) + if isLower { + onChange(min(snapped, upper), upper) + } else { + onChange(lower, max(snapped, lower)) + } + } + + /// The value at `offset` points along a `track` that spans `bounds`. + static func value(at offset: CGFloat, track: CGFloat, bounds: ClosedRange) -> Double { + bounds.lowerBound + Double(offset / track) * (bounds.upperBound - bounds.lowerBound) + } + + static func clamp(_ value: Double, to bounds: ClosedRange) -> Double { + min(max(value, bounds.lowerBound), bounds.upperBound) + } + + /// The nearest step to `value`, within `bounds`. + static func snap(_ value: Double, bounds: ClosedRange, step: Double) -> Double { + guard step > 0 else { return clamp(value, to: bounds) } + let steps = ((value - bounds.lowerBound) / step).rounded() + return clamp(bounds.lowerBound + steps * step, to: bounds) + } +} + +// MARK: - Buttons + +struct ButtonView: View { + let props: ComponentProps + @Environment(\.openUIButtonFillsWidth) private var fillsWidth + @Environment(OpenUIContext.self) private var context + @Environment(\.openUIFormName) private var form + @Environment(FormValidation.self) private var validation: FormValidation? + + var body: some View { + let label = props.text("label") + let variant = props.string("variant") ?? "primary" + let destructive = props.string("type") == "destructive" + Button(role: destructive ? .destructive : nil) { + let action = props["action"] + if let validation, variant == "primary", !shouldSkipValidation(action) { + guard validation.validateForm() else { return } + } + context.triggerAction(label, form: form, action: action.isNullish ? nil : action) + } label: { + Text(label).frame(maxWidth: fillsWidth ? .infinity : nil) + } + .modifier(ButtonVariant(variant: variant)) + .controlSize(controlSize) + .disabled(context.isStreaming) + } + + /// Primary buttons validate the form first, unless their action plan only + /// has steps that don't submit anything (no @ToAssistant, no mutation). + private func shouldSkipValidation(_ action: OpenUIValue) -> Bool { + guard case .actionPlan(let plan) = action else { return false } + return !plan.steps.contains { step in + switch step { + case .continueConversation: return true + case .run(_, let refType): return refType == .mutation + default: return false + } + } + } + + private var controlSize: ControlSize { + switch props.string("size") { + case "extra-small": return .mini + case "small": return .small + case "large": return .large + default: return .regular + } + } +} + +private struct ButtonFillsWidthKey: EnvironmentKey { + static let defaultValue = false +} + +extension EnvironmentValues { + /// Whether buttons stretch to the available width, like a card footer's. + var openUIButtonFillsWidth: Bool { + get { self[ButtonFillsWidthKey.self] } + set { self[ButtonFillsWidthKey.self] = newValue } + } +} + +private struct ButtonVariant: ViewModifier { + let variant: String + + func body(content: Content) -> some View { + switch variant { + case "secondary": content.buttonStyle(.bordered) + case "tertiary", "ghost": content.buttonStyle(.borderless) + default: content.modifier(ProminentButton()) + } + } +} + +/// A filled button in the theme's accent, with its `onAccent` label. +struct ProminentButton: ViewModifier { + @Environment(\.openUITheme) private var theme + @Environment(\.isEnabled) private var isEnabled + #if os(macOS) + @Environment(\.controlActiveState) private var activeState + #endif + + func body(content: Content) -> some View { + content.buttonStyle(.borderedProminent).foregroundStyle(labelColor) + } + + /// The system draws a disabled button, or any prominent button in an + /// inactive Mac window, gray instead of in the accent. The accent's label + /// color would be unreadable on that, so those keep the system's. + private var labelColor: Color { + #if os(macOS) + if activeState == .inactive { return .primary } + #endif + return isEnabled ? theme.onAccent : .secondary + } +} + +struct ButtonsView: View { + let props: ComponentProps + @Environment(\.openUITheme) private var theme + + var body: some View { + let buttons = props.array("buttons") + if props.string("direction") == "column" { + VStack(alignment: .leading, spacing: theme.compactSpacing) { + ForEach(NodeItem.list(buttons)) { OpenUINode($0.value) } + } + } else { + FlowLayout(spacing: theme.compactSpacing) { + ForEach(NodeItem.list(buttons)) { OpenUINode($0.value) } + } + } + } +} + +/// react-ui's slider labels: 1000 and up in short compact notation ("10k", +/// "2.5m"), smaller numbers as JavaScript prints them. +func compactNumber(_ number: Double) -> String { + guard number >= 1000 else { return jsNumberToString(number) } + return number.formatted( + .number.notation(.compactName).locale(Locale(identifier: "en_US")) + ).lowercased() +} + +/// react-ui's field look: a faint fill, a 1pt border and 10pt corners. +private struct FieldSurface: ViewModifier { + var border: Color? + @Environment(\.openUITheme) private var theme + + func body(content: Content) -> some View { + content + .padding(.horizontal, 10) + .padding(.vertical, 8) + .background(theme.subtleSurface, in: RoundedRectangle(cornerRadius: 10)) + .overlay( + RoundedRectangle(cornerRadius: 10).strokeBorder(border ?? theme.interactiveBorder)) + } +} + +extension View { + fileprivate func fieldSurface(border: Color? = nil) -> some View { + modifier(FieldSurface(border: border)) + } +} diff --git a/packages/swift-lang/Sources/OpenUISwiftUI/Components/IconComponents.swift b/packages/swift-lang/Sources/OpenUISwiftUI/Components/IconComponents.swift new file mode 100644 index 0000000000..de31552c2c --- /dev/null +++ b/packages/swift-lang/Sources/OpenUISwiftUI/Components/IconComponents.swift @@ -0,0 +1,455 @@ +import OpenUILang +import SwiftUI + +#if canImport(UIKit) + import UIKit +#else + import AppKit +#endif + +/// Maps lucide icon names (what the model writes) to SF Symbols, with the +/// same category fallbacks as react-ui: an unknown name falls back to its +/// category's representative icon, then to `circle-dot`. +enum LucideSymbols { + static func systemName(for name: String, category: String?) -> String { + let key = name.trimmingCharacters(in: .whitespaces).lowercased() + if let symbol = symbols[key] ?? symbols[aliases[key] ?? ""], exists(symbol) { return symbol } + let fallback = categoryFallbacks[ + category?.trimmingCharacters(in: .whitespaces).lowercased() ?? ""] + if let fallback, let symbol = symbols[fallback], exists(symbol) { return symbol } + return symbols[defaultFallback]! + } + + /// Older OS versions lack some symbols; fall back rather than show nothing. + private static func exists(_ symbol: String) -> Bool { + #if canImport(UIKit) + UIImage(systemName: symbol) != nil + #else + NSImage(systemSymbolName: symbol, accessibilityDescription: nil) != nil + #endif + } + + static let defaultFallback = "circle-dot" + + /// Lucide's renamed icons, mapped to the names in `symbols`. + static let aliases: [String: String] = [ + "triangle-alert": "alert-triangle", "circle-alert": "alert-circle", + "check-circle-2": "check-circle", + "circle-check-big": "circle-check", "chart-no-axes-column": "bar-chart", + "settings-2": "settings", + "house-plus": "house", "utensils-crossed": "utensils", "file-chart-column": "file-text", + "mail-open": "mail", "map-pinned": "map-pin", "sparkle": "sparkles", "dollar": "dollar-sign", + ] + + /// react-ui's `categoryFallbacks`: category → representative lucide name. + static let categoryFallbacks: [String: String] = [ + "accessibility": "accessibility", "account": "user", "animals": "cat", "arrows": "arrow-right", + "brands": "box", "buildings": "building", "charts": "chart-line", + "communication": "message-square", "connectivity": "wifi", "cursors": "mouse-pointer", + "design": "palette", "development": "code", "devices": "smartphone", "emoji": "smile", + "files": "file", "finance": "dollar-sign", "food-beverage": "cooking-pot", "gaming": "gamepad", + "home": "house", "layout": "layout", "mail": "mail", "math": "calculator", + "medical": "heart-pulse", "multimedia": "music", "nature": "tree-pine", + "navigation": "map-pin", "notifications": "bell", "people": "users", + "photography": "camera", "science": "microscope", "seasons": "sun", "security": "shield", + "shapes": "circle", "shopping": "shopping-cart", "social": "share-2", "sports": "trophy", + "sustainability": "leaf", "text": "type", "time": "clock", "tools": "wrench", + "transportation": "car", "travel": "plane", "weather": "cloud", + ] + + static let symbols: [String: String] = [ + "activity": "waveform.path.ecg", + "alarm-clock": "alarm", + "alert-circle": "exclamationmark.circle", + "alert-triangle": "exclamationmark.triangle", + "archive": "archivebox", + "arrow-down": "arrow.down", + "arrow-down-right": "arrow.down.right", + "arrow-left": "arrow.left", + "arrow-right": "arrow.right", + "arrow-up": "arrow.up", + "arrow-up-right": "arrow.up.right", + "award": "rosette", + "baby": "figure.and.child.holdinghands", + "backpack": "backpack", + "badge-check": "checkmark.seal", + "banknote": "banknote", + "bar-chart": "chart.bar", + "bar-chart-2": "chart.bar", + "bath": "bathtub", + "battery": "battery.100", + "bed": "bed.double", + "beer": "mug", + "bell": "bell", + "bike": "bicycle", + "book": "book", + "book-open": "book", + "bookmark": "bookmark", + "box": "shippingbox", + "brain": "brain", + "briefcase": "briefcase", + "bug": "ant", + "building": "building", + "building-2": "building.2", + "bus": "bus", + "cake": "birthday.cake", + "calculator": "function", + "calendar": "calendar", + "calendar-days": "calendar", + "camera": "camera", + "car": "car", + "cart": "cart", + "cat": "cat", + "chart-bar": "chart.bar", + "chart-column": "chart.bar", + "chart-line": "chart.xyaxis.line", + "chart-pie": "chart.pie", + "check": "checkmark", + "check-circle": "checkmark.circle", + "chevron-down": "chevron.down", + "chevron-left": "chevron.left", + "chevron-right": "chevron.right", + "chevron-up": "chevron.up", + "circle": "circle", + "circle-alert": "exclamationmark.circle", + "circle-check": "checkmark.circle", + "circle-dollar-sign": "dollarsign.circle", + "circle-dot": "smallcircle.filled.circle", + "circle-help": "questionmark.circle", + "circle-x": "xmark.circle", + "clapperboard": "movieclapper", + "clipboard": "clipboard", + "clock": "clock", + "cloud": "cloud", + "cloud-rain": "cloud.rain", + "cloud-sun": "cloud.sun", + "code": "chevron.left.forwardslash.chevron.right", + "coffee": "cup.and.saucer", + "coins": "dollarsign.circle", + "compass": "safari", + "cooking-pot": "frying.pan", + "copy": "doc.on.doc", + "cpu": "cpu", + "credit-card": "creditcard", + "crown": "crown", + "database": "cylinder", + "disc-3": "opticaldisc", + "dollar-sign": "dollarsign", + "download": "arrow.down.to.line", + "droplet": "drop", + "dumbbell": "dumbbell", + "edit": "pencil", + "euro": "eurosign", + "external-link": "arrow.up.right.square", + "eye": "eye", + "eye-off": "eye.slash", + "figma": "paintbrush", + "file": "doc", + "file-text": "doc.text", + "film": "film", + "filter": "line.3.horizontal.decrease", + "fish": "fish", + "flag": "flag", + "flame": "flame", + "flask-conical": "flask", + "folder": "folder", + "gamepad": "gamecontroller", + "gamepad-2": "gamecontroller", + "gauge": "gauge.with.dots.needle.33percent", + "gift": "gift", + "git-branch": "arrow.triangle.branch", + "glasses": "eyeglasses", + "globe": "globe", + "graduation-cap": "graduationcap", + "grid": "square.grid.2x2", + "hammer": "hammer", + "hand": "hand.raised", + "hard-drive": "internaldrive", + "hash": "number", + "headphones": "headphones", + "heart": "heart", + "heart-pulse": "heart.text.square", + "help-circle": "questionmark.circle", + "history": "clock.arrow.circlepath", + "home": "house", + "hotel": "bed.double", + "house": "house", + "image": "photo", + "inbox": "tray", + "info": "info.circle", + "key": "key", + "landmark": "building.columns", + "laptop": "laptopcomputer", + "layers": "square.3.layers.3d", + "layout": "rectangle.3.group", + "layout-dashboard": "rectangle.3.group", + "leaf": "leaf", + "lightbulb": "lightbulb", + "line-chart": "chart.xyaxis.line", + "link": "link", + "list": "list.bullet", + "list-checks": "checklist", + "loader": "arrow.triangle.2.circlepath", + "lock": "lock", + "log-in": "arrow.right.to.line", + "log-out": "rectangle.portrait.and.arrow.right", + "mail": "envelope", + "map": "map", + "map-pin": "mappin.and.ellipse", + "medal": "medal", + "megaphone": "megaphone", + "menu": "line.3.horizontal", + "message-circle": "message", + "message-square": "bubble.left", + "mic": "mic", + "microscope": "microbe", + "minus": "minus", + "monitor": "display", + "moon": "moon", + "more-horizontal": "ellipsis", + "mountain": "mountain.2", + "mouse-pointer": "cursorarrow", + "music": "music.note", + "navigation": "location.north", + "newspaper": "newspaper", + "notebook": "book.closed", + "package": "shippingbox", + "palette": "paintpalette", + "paperclip": "paperclip", + "pause": "pause", + "pen": "pencil", + "pencil": "pencil", + "percent": "percent", + "phone": "phone", + "pie-chart": "chart.pie", + "pill": "pills", + "pizza": "fork.knife", + "plane": "airplane", + "play": "play", + "plug": "powerplug", + "plus": "plus", + "pound-sterling": "sterlingsign", + "power": "power", + "printer": "printer", + "puzzle": "puzzlepiece", + "receipt": "receipt", + "refresh-cw": "arrow.clockwise", + "rocket": "paperplane", + "rotate-ccw": "arrow.counterclockwise", + "route": "point.topleft.down.to.point.bottomright.curvepath", + "ruler": "ruler", + "save": "square.and.arrow.down", + "scale": "scalemass", + "school": "building.columns", + "scissors": "scissors", + "search": "magnifyingglass", + "send": "paperplane", + "server": "server.rack", + "settings": "gearshape", + "share": "square.and.arrow.up", + "share-2": "square.and.arrow.up", + "shield": "shield", + "shield-check": "checkmark.shield", + "ship": "ferry", + "shirt": "tshirt", + "shopping-bag": "bag", + "shopping-cart": "cart", + "smartphone": "iphone", + "smile": "face.smiling", + "snowflake": "snowflake", + "sparkles": "sparkles", + "speaker": "hifispeaker", + "square": "square", + "star": "star", + "stethoscope": "stethoscope", + "store": "storefront", + "sun": "sun.max", + "sunrise": "sunrise", + "sunset": "sunset", + "swords": "figure.fencing", + "tablet": "ipad", + "tag": "tag", + "target": "scope", + "tent": "tent", + "terminal": "apple.terminal", + "thermometer": "thermometer.medium", + "thumbs-down": "hand.thumbsdown", + "thumbs-up": "hand.thumbsup", + "ticket": "ticket", + "timer": "timer", + "train": "tram", + "train-front": "tram", + "trash": "trash", + "trash-2": "trash", + "tree-pine": "tree", + "trees": "tree", + "trending-down": "chart.line.downtrend.xyaxis", + "trending-up": "chart.line.uptrend.xyaxis", + "trophy": "trophy", + "truck": "truck.box", + "tv": "tv", + "type": "textformat", + "umbrella": "umbrella", + "upload": "arrow.up.to.line", + "user": "person", + "user-check": "person.crop.circle.badge.checkmark", + "user-minus": "person.badge.minus", + "user-plus": "person.badge.plus", + "users": "person.2", + "utensils": "fork.knife", + "video": "video", + "volume-2": "speaker.wave.2", + "wallet": "wallet.bifold", + "watch": "applewatch", + "waves": "water.waves", + "wifi": "wifi", + "wind": "wind", + "wine": "wineglass", + "wrench": "wrench.adjustable", + "x": "xmark", + "zap": "bolt", + "accessibility": "accessibility", + "mouse": "computermouse", + ] +} + +/// The SF Symbol for an `Icon` element value (or nil when the value isn't one). +func iconSymbol(_ value: OpenUIValue) -> String? { + guard let icon = value.elementValue, icon.typeName == "Icon", + let name = icon.props["name"]?.stringValue, !name.isEmpty + else { return nil } + return LucideSymbols.systemName(for: name, category: icon.props["category"]?.stringValue) +} + +struct IconView: View { + let props: ComponentProps + + var body: some View { + if let name = props.string("name"), !name.isEmpty { + Image(systemName: LucideSymbols.systemName(for: name, category: props.string("category"))) + } + } +} + +struct IconButtonView: View { + let props: ComponentProps + @Environment(OpenUIContext.self) private var context + @Environment(\.openUIFormName) private var form + + var body: some View { + let label = props.text("name") + let symbol = + iconSymbol(props["icon"]) ?? LucideSymbols.systemName(for: "circle-dot", category: nil) + let circle = props.string("shape") == "circle" + Button { + let action = props["action"] + context.triggerAction(label, form: form, action: action.isNullish ? nil : action) + } label: { + Image(systemName: symbol) + .frame(width: 18, height: 18) + .padding(4) + } + .modifier(IconButtonStyle(variant: props.string("variant") ?? "primary", circle: circle)) + .controlSize(controlSize) + .accessibilityLabel(label) + .disabled(context.isStreaming) + } + + private var controlSize: ControlSize { + switch props.string("size") { + case "extra-small": return .mini + case "small": return .small + case "large": return .large + default: return .regular + } + } +} + +private struct IconButtonStyle: ViewModifier { + let variant: String + let circle: Bool + + func body(content: Content) -> some View { + let styled = Group { + switch variant { + case "secondary": content.buttonStyle(.bordered) + case "tertiary": content.buttonStyle(.borderless) + default: content.modifier(ProminentButton()) + } + } + if circle { + styled.buttonBorderShape(.circle) + } else { + styled.buttonBorderShape(.roundedRectangle) + } + } +} + +struct TagView: View { + let props: ComponentProps + var body: some View { + TagLabel( + text: props.text("text"), symbol: iconSymbol(props["icon"]), size: props.string("size"), + variant: props.string("variant")) + } +} + +/// A small pill with optional icon, tinted by status variant. +struct TagLabel: View { + let text: String + let symbol: String? + let size: String? + let variant: String? + @Environment(\.openUITagOnImage) private var onImage + @Environment(\.openUITheme) private var theme + + var body: some View { + let neutral = variant == nil || variant == "neutral" + let color = neutral ? Color.secondary : theme.status(variant) + HStack(spacing: 4) { + if let symbol { Image(systemName: symbol) } + // One line, cut short when the tag would be wider than its container. + Text(text).lineLimit(1) + } + .font(font) + .foregroundStyle(onImage ? Color.white : neutral ? Color.primary : color) + .padding(tagPadding(size)) + // On a photo the variant tint isn't readable, so react-ui switches every + // tag to white on translucent black there. + .background( + onImage ? Color.black.opacity(0.4) : color.opacity(0.12), + in: RoundedRectangle(cornerRadius: tagRadius(size))) + } + + private var font: Font { + switch size { + case "sm": return .caption2.weight(.medium) + case "lg": return .callout.weight(.medium) + default: return .caption.weight(.medium) + } + } +} + +private struct TagOnImageKey: EnvironmentKey { + static let defaultValue = false +} + +extension EnvironmentValues { + /// True for tags drawn over an image, like a visual card's. + var openUITagOnImage: Bool { + get { self[TagOnImageKey.self] } + set { self[TagOnImageKey.self] = newValue } + } +} + +/// react-ui's tag padding per size: 4/6, 6/8 and 8/12. +func tagPadding(_ size: String?) -> EdgeInsets { + switch size { + case "sm": return EdgeInsets(top: 4, leading: 6, bottom: 4, trailing: 6) + case "lg": return EdgeInsets(top: 8, leading: 12, bottom: 8, trailing: 12) + default: return EdgeInsets(top: 6, leading: 8, bottom: 6, trailing: 8) + } +} + +/// react-ui's tag corner radius: 6, or 8 for large tags. +func tagRadius(_ size: String?) -> CGFloat { size == "lg" ? 8 : 6 } diff --git a/packages/swift-lang/Sources/OpenUISwiftUI/Components/LayoutComponents.swift b/packages/swift-lang/Sources/OpenUISwiftUI/Components/LayoutComponents.swift new file mode 100644 index 0000000000..984aa34ba8 --- /dev/null +++ b/packages/swift-lang/Sources/OpenUISwiftUI/Components/LayoutComponents.swift @@ -0,0 +1,611 @@ +import OpenUILang +import SwiftUI + +// MARK: - Table + +/// Column-oriented table: each `Col` holds its own data array. Cells can be +/// text or elements (e.g. buttons from `@Each`). Long tables page by 10 rows. +struct TableView: View { + let props: ComponentProps + @State private var page = 0 + @Environment(OpenUIContext.self) private var context + @Environment(\.openUITheme) private var theme + + private static let pageSize = 10 + + var body: some View { + let columns = props.children("columns") + let data = columns.map { column -> [OpenUIValue] in + let raw = column["data"] + if case .array(let items) = raw { return items } + return raw.isNullish ? [] : [raw] + } + let rowCount = data.map(\.count).max() ?? 0 + let pages = max(1, Int((Double(rowCount) / Double(Self.pageSize)).rounded(.up))) + let current = min(page, pages - 1) + let rows = Array( + stride(from: current * Self.pageSize, to: min(rowCount, (current + 1) * Self.pageSize), by: 1) + ) + + if context.isQueryLoading && rowCount == 0 { + // Placeholder rows while a Query fills the table, like react-ui's skeleton. + VStack(alignment: .leading, spacing: 10) { + ForEach(0..<5, id: \.self) { _ in SkeletonBlock(height: 14, cornerRadius: 4) } + } + } else if !columns.isEmpty { + VStack(alignment: .leading, spacing: theme.compactSpacing) { + // Full width like react-ui's table; scrolls sideways when the columns + // don't fit. + ViewThatFits(in: .horizontal) { + grid(columns, data, rows) + ScrollView(.horizontal, showsIndicators: false) { + grid(columns, data, rows).fixedSize(horizontal: true, vertical: false) + } + } + .clipShape(RoundedRectangle(cornerRadius: 14)) + .overlay(RoundedRectangle(cornerRadius: 14).strokeBorder(theme.border)) + if pages > 1 { + HStack(spacing: 8) { + Spacer() + Button("Previous page", systemImage: "chevron.left") { page = max(0, current - 1) } + .disabled(current == 0) + Text("\(current + 1) / \(pages)").font(.caption.monospacedDigit()) + .foregroundStyle(.secondary) + Button("Next page", systemImage: "chevron.right") { + page = min(pages - 1, current + 1) + } + .disabled(current >= pages - 1) + } + .labelStyle(.iconOnly) + .buttonStyle(.bordered) + .controlSize(.small) + } + } + } + } + + private func grid(_ columns: [ComponentProps], _ data: [[OpenUIValue]], _ rows: [Int]) + -> some View + { + Grid(alignment: .leading, horizontalSpacing: 0, verticalSpacing: 0) { + GridRow { + ForEach(columns.indices, id: \.self) { index in + Text(columns[index].text("label")) + .font(.subheadline) + .foregroundStyle(.secondary) + .padding(12) + .frame(maxWidth: .infinity, alignment: .leading) + } + } + theme.border.frame(height: 1) + ForEach(rows.indices, id: \.self) { position in + let row = rows[position] + GridRow { + ForEach(columns.indices, id: \.self) { index in + cell(row < data[index].count ? data[index][row] : .null) + .padding(12) + .frame(maxWidth: .infinity, alignment: .leading) + // Every second row is tinted, as in react-ui. + .background(position % 2 == 1 ? theme.subtleSurface : .clear) + } + } + if position < rows.count - 1 { theme.border.frame(height: 1) } + } + } + } + + @ViewBuilder + private func cell(_ value: OpenUIValue) -> some View { + switch value { + case .element, .array: + OpenUINode(value) + default: + Text(displayText(value)).font(.subheadline) + } + } +} + +struct ColView: View { + let props: ComponentProps + + var body: some View { + VStack(alignment: .leading, spacing: 4) { + Text(props.text("label")).font(.subheadline.weight(.semibold)) + OpenUINodes(props.array("data"), spacing: 4) + } + } +} + +// MARK: - Lists + +struct ListBlockView: View { + let props: ComponentProps + @Environment(OpenUIContext.self) private var context + + var body: some View { + let numbered = props.string("variant") != "image" + let small = props.string("size") == "small" + VStack(alignment: .leading, spacing: small ? 6 : 10) { + ForEach(Array(props.children("items").enumerated()), id: \.offset) { index, item in + ListRow( + item: item, marker: numbered ? "\(index + 1)" : nil, small: small, + onTap: item["action"].isNullish + ? nil + : { context.triggerAction(item.text("title"), action: item["action"]) }) + } + } + } +} + +struct ListItemView: View { + let props: ComponentProps + @Environment(OpenUIContext.self) private var context + + var body: some View { + ListRow( + item: props, marker: nil, small: false, + onTap: props["action"].isNullish + ? nil : { context.triggerAction(props.text("title"), action: props["action"]) }) + } +} + +/// A list row: marker or image, title and subtitle, clickable only with an action. +private struct ListRow: View { + let item: ComponentProps + let marker: String? + let small: Bool + let onTap: (() -> Void)? + @Environment(\.openUITheme) private var theme + + var body: some View { + if let onTap { + Button(action: onTap) { Hovering(content: row) }.buttonStyle(.openUIHover) + } else { + row(hovered: false) + } + } + + /// react-ui grows a clickable row's marker and deepens its fill under the + /// pointer. + private func row(hovered: Bool) -> some View { + HStack(alignment: .top, spacing: 10) { + if let marker { + Text(marker) + .font(.caption.weight(.semibold).monospacedDigit()) + .frame(width: 22, height: 22) + .background(hovered ? Color.primary.opacity(0.1) : theme.sunkSurface, in: Circle()) + .scaleEffect(hovered ? 1.1 : 1) + } else if let src = item["image"]["src"].stringValue { + RemoteImage(src: src, alt: displayText(item["image"]["alt"])) + .frame(width: 44, height: 44) + .scaleEffect(hovered ? 1.1 : 1) + } + VStack(alignment: .leading, spacing: 2) { + Text(item.text("title")).font(small ? .subheadline : .body) + if let subtitle = item.string("subtitle"), !subtitle.isEmpty { + Text(subtitle).font(.caption).foregroundStyle(.secondary) + } + } + Spacer(minLength: 0) + if onTap != nil { + if let actionLabel = item.string("actionLabel"), !actionLabel.isEmpty { + Text(actionLabel).font(.caption.weight(.medium)).foregroundStyle(theme.accent) + } else { + Image(systemName: "chevron.right").font(.caption).foregroundStyle(.secondary) + } + } + } + .contentShape(Rectangle()) + } +} + +/// "Related Queries" and a divided list of suggestions, like react-ui's +/// FollowUpBlock; tapping one sends its text as the user's message. +struct FollowUpBlockView: View { + let props: ComponentProps + @Environment(OpenUIContext.self) private var context + @Environment(\.openUITheme) private var theme + + var body: some View { + let items = props.children("items") + VStack(alignment: .leading, spacing: 0) { + Text("Related Queries") + .foregroundStyle(.secondary) + .padding(.vertical, 8) + theme.border.frame(height: 1) + ForEach(items.indices, id: \.self) { index in + let text = items[index].text("text") + FollowUpButton(text: text) { context.triggerAction(text) } + if index < items.count - 1 { theme.border.frame(height: 1) } + } + } + .disabled(context.isStreaming) + } +} + +struct FollowUpItemView: View { + let props: ComponentProps + @Environment(OpenUIContext.self) private var context + + var body: some View { + FollowUpButton(text: props.text("text")) { context.triggerAction(props.text("text")) } + .disabled(context.isStreaming) + } +} + +/// A suggested next message; tapping it sends the text as the user's message. +private struct FollowUpButton: View { + let text: String + let action: () -> Void + + var body: some View { + Button(action: action) { + Text(text) + .multilineTextAlignment(.leading) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.vertical, 8) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + } +} + +struct StepsView: View { + let props: ComponentProps + @Environment(\.openUITheme) private var theme + + var body: some View { + let items = props.children("items") + VStack(alignment: .leading, spacing: 0) { + ForEach(Array(items.enumerated()), id: \.offset) { index, item in + HStack(alignment: .top, spacing: 12) { + VStack(spacing: 0) { + Text("\(index + 1)") + .font(.caption.weight(.bold).monospacedDigit()) + .foregroundStyle(theme.onAccent) + .frame(width: 22, height: 22) + .background(theme.accent, in: Circle()) + if index < items.count - 1 { + Rectangle().fill(.secondary.opacity(0.3)).frame(width: 2).frame(maxHeight: .infinity) + } + } + StepContent(item: item).padding(.bottom, index < items.count - 1 ? 14 : 0) + } + } + } + } +} + +struct StepsItemView: View { + let props: ComponentProps + var body: some View { StepContent(item: props) } +} + +private struct StepContent: View { + let item: ComponentProps + + var body: some View { + VStack(alignment: .leading, spacing: 2) { + Text(item.text("title")).font(.subheadline.weight(.semibold)) + InlineMarkdown(item.text("details")).font(.subheadline).foregroundStyle(.secondary) + } + .frame(maxWidth: .infinity, alignment: .leading) + } +} + +// MARK: - Tabs, accordions and sections + +/// Underlined tabs. Until the user picks one, the selection follows the stream +/// to whichever tab's content grew last, like react-ui's Tabs. +struct TabsView: View { + let props: ComponentProps + @State private var active: String? + @State private var userChose = false + @State private var contentSizes: [String: Int] = [:] + @Environment(\.openUITheme) private var theme + + var body: some View { + let items = props.children("items").filter { !$0["value"].isNullish } + let sizes = items.map { + (value: $0.text("value"), size: JSON.stringify($0["content"]).utf16.count) + } + if !items.isEmpty { + VStack(alignment: .leading, spacing: theme.spacing) { + ScrollViewReader { proxy in + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 20) { + ForEach(items.indices, id: \.self) { index in + tab(items[index]).id(items[index].text("value")) + } + } + } + // Keep the active tab in view as the selection follows the stream. + .onChange(of: active) { + withAnimation { proxy.scrollTo(active, anchor: .center) } + } + } + if let item = items.first(where: { $0.text("value") == active }) { + OpenUINodes(item.array("content")) + } + } + .onChange(of: sizes.map(\.size), initial: true) { followStream(sizes) } + } + } + + private func tab(_ item: ComponentProps) -> some View { + let value = item.text("value") + let isActive = value == active + return Button { + userChose = true + withOpenUIAnimation(Motion.reveal) { active = value } + } label: { + Hovering { hovered in + Text(item.text("trigger")) + .font(.subheadline.weight(isActive ? .semibold : .regular)) + .foregroundStyle(isActive || hovered ? .primary : .secondary) + .padding(.vertical, 6) + .overlay(alignment: .bottom) { + if isActive { Rectangle().fill(.primary).frame(height: 2) } + } + } + } + .buttonStyle(.openUIHover) + } + + private func followStream(_ sizes: [(value: String, size: Int)]) { + if active == nil { active = sizes.first?.value } + guard !userChose else { return } + var candidate: String? + for (value, size) in sizes where size > (contentSizes[value] ?? 0) { candidate = value } + contentSizes = Dictionary(sizes.map { ($0.value, $0.size) }, uniquingKeysWith: { $1 }) + if let candidate, candidate != active { + withOpenUIAnimation(Motion.reveal) { active = candidate } + } + } +} + +/// One item open at a time. Until the user opens or closes one, the newest +/// item opens as it streams in, like react-ui's Accordion. +struct AccordionView: View { + let props: ComponentProps + @State private var open: String? + @State private var userChose = false + @State private var seenCount = 0 + + var body: some View { + let items = props.children("items") + VStack(alignment: .leading, spacing: 0) { + ForEach(items.indices, id: \.self) { index in + let value = items[index].text("value") + Disclosure( + trigger: items[index].text("trigger"), content: items[index].array("content"), + isOpen: Binding( + get: { open == value }, + set: { isOpen in + userChose = true + open = isOpen ? value : nil + })) + if index < items.count - 1 { Divider() } + } + } + .onChange(of: items.count, initial: true) { + if !userChose, items.count > seenCount, let newest = items.last { + withOpenUIAnimation(Motion.reveal) { open = newest.text("value") } + } + seenCount = items.count + } + } +} + +/// Sections open as they stream in, then fold back to the first one when the +/// response finishes (unless the user opened or closed one), like react-ui's +/// SectionBlock. With `isFoldable: false` every section is shown. +struct SectionBlockView: View { + let props: ComponentProps + @State private var open: Set = [] + @State private var userChose = false + @State private var seenCount = 0 + @Environment(OpenUIContext.self) private var context + @Environment(\.openUITheme) private var theme + + var body: some View { + let sections = props.children("sections") + let values = sections.indices.map { sectionValue(sections[$0], $0) } + if props.bool("isFoldable") == false { + VStack(alignment: .leading, spacing: theme.spacing) { + ForEach(sections.indices, id: \.self) { index in + VStack(alignment: .leading, spacing: theme.compactSpacing) { + Text(sections[index].text("trigger")).font(.headline) + OpenUINodes(sections[index].array("content")) + } + } + } + } else { + VStack(alignment: .leading, spacing: 0) { + ForEach(sections.indices, id: \.self) { index in + let value = values[index] + Disclosure( + trigger: sections[index].text("trigger"), content: sections[index].array("content"), + isOpen: Binding( + get: { open.contains(value) }, + set: { isOpen in + userChose = true + if isOpen { open.insert(value) } else { open.remove(value) } + })) + if index < sections.count - 1 { Divider() } + } + } + .onChange(of: values.count, initial: true) { + if context.isStreaming, values.count > seenCount, !userChose, let last = values.last { + withOpenUIAnimation(Motion.reveal) { _ = open.insert(last) } + } else if open.isEmpty, let first = values.first { + open = [first] + } + seenCount = values.count + } + .onChange(of: context.isStreaming) { wasStreaming, isStreaming in + if wasStreaming, !isStreaming, !userChose, let first = values.first { + withOpenUIAnimation(Motion.reveal) { open = [first] } + } + } + } + } + + private func sectionValue(_ section: ComponentProps, _ index: Int) -> String { + section["value"].isNullish ? String(index) : section.text("value") + } +} + +/// A sub-item rendered on its own: its trigger as a heading, then its content. +struct TriggeredContentView: View { + let props: ComponentProps + @Environment(\.openUITheme) private var theme + + var body: some View { + VStack(alignment: .leading, spacing: theme.compactSpacing) { + Text(props.text("trigger")).font(.headline) + OpenUINodes(props.array("content")) + } + } +} + +private struct Disclosure: View { + let trigger: String + let content: [OpenUIValue] + @Binding var isOpen: Bool + + var body: some View { + DisclosureGroup(isExpanded: $isOpen) { + OpenUINodes(content).padding(.top, 6) + } label: { + Text(trigger).font(.subheadline.weight(.semibold)) + } + .padding(.vertical, 8) + } +} + +// MARK: - Carousel + +/// Slides scroll horizontally, like react-ui's Carousel: 280-point cards +/// (248 on narrow screens) with buttons to step through them. +struct CarouselView: View { + let props: ComponentProps + @Environment(\.openUITheme) private var theme + + var body: some View { + let slides = props.array("children") + let fill = props.string("variant") == "sunk" ? theme.subtleSurface : theme.surface + CarouselScroller( + count: slides.count, spacing: 12, fade: 40, showsButtons: true, equalHeights: true + ) { + index in + OpenUINodes(slides[index].arrayValue ?? [slides[index]]) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + .padding(12) + .background(fill, in: RoundedRectangle(cornerRadius: 14)) + .overlay(RoundedRectangle(cornerRadius: 14).strokeBorder(theme.border)) + .containerRelativeFrame(.horizontal) { width, _ in width <= 400 ? 248 : 280 } + } + } +} + +/// A horizontal row that scrolls like react-ui's carousels: items snap to the +/// leading edge, an edge fades out where there's more to scroll, and buttons +/// step one item at a time. Whether it can scroll only drives the fades and +/// buttons, never a size, so it can't feed back into layout. +struct CarouselScroller: View { + let count: Int + var spacing: CGFloat + /// How far an edge fades out: 40 for react-ui's Carousel, its `space-3xl` + /// (48) for card blocks. + var fade: CGFloat + /// Shows the buttons all the time, as react-ui's Carousel does. Card blocks + /// show them only while a pointer is over the row: a mouse wheel scrolls + /// up and down, so without them a mouse can't reach the cards past the + /// edge (a browser has shift-scrolling and a scrollbar for that). + var showsButtons = false + /// Stretches items to the tallest one, as a flex row does. + var equalHeights = false + var verticalPadding: CGFloat = 2 + @ViewBuilder let item: (Int) -> Item + @State private var position: Int? + @State private var content = CGRect.zero + @State private var visibleWidth: CGFloat = 0 + @State private var hoveringRow = false + /// The buttons hang half outside the row, so the pointer can be on one + /// without being over the row. + @State private var hoveringButton = false + @Environment(\.openUITheme) private var theme + + var body: some View { + let canScrollBack = content.minX < -1 + let canScrollOn = content.maxX > visibleWidth + 1 + let buttons = showsButtons || hoveringRow || hoveringButton + ScrollView(.horizontal, showsIndicators: false) { + HStack(alignment: .top, spacing: spacing) { + ForEach(0.. some View { + Button { + withOpenUIAnimation(Motion.reveal) { + position = min(max((position ?? 0) + step, 0), count - 1) + } + } label: { + Image(systemName: step < 0 ? "chevron.left" : "chevron.right") + .font(.footnote.weight(.semibold)) + .frame(width: 28, height: 28) + .background(theme.surface, in: RoundedRectangle(cornerRadius: theme.smallCornerRadius)) + .overlay( + RoundedRectangle(cornerRadius: theme.smallCornerRadius).strokeBorder( + theme.interactiveBorder) + ) + .shadow(color: .black.opacity(0.08), radius: 3, y: 1) + } + .buttonStyle(.plain) + .onHover { hoveringButton = $0 } + // A button that goes away under the pointer (the row reached its end) + // never hears the pointer leave. + .onDisappear { hoveringButton = false } + .accessibilityLabel(step < 0 ? "Previous" : "Next") + .offset(x: step < 0 ? -12 : 12) + } +} diff --git a/packages/swift-lang/Sources/OpenUISwiftUI/Components/SelectionComponents.swift b/packages/swift-lang/Sources/OpenUISwiftUI/Components/SelectionComponents.swift new file mode 100644 index 0000000000..4812a73826 --- /dev/null +++ b/packages/swift-lang/Sources/OpenUISwiftUI/Components/SelectionComponents.swift @@ -0,0 +1,341 @@ +import OpenUILang +import SwiftUI + +// MARK: - Shared selection model + +/// The selected values of a single- or multiple-choice field, following +/// react-ui's `normalizeSelection`: a single choice is stored as a string, +/// multiple choices as a string array; unset fields fall back to `defaultValue`. +private func selection(stored: OpenUIValue, defaultValue: OpenUIValue, single: Bool) -> [String] { + func strings(_ value: OpenUIValue) -> [String]? { + switch value { + case .string(let string): return string.isEmpty ? [] : [string] + case .array(let items): return items.compactMap(\.stringValue) + default: return nil + } + } + let values = strings(stored) ?? strings(defaultValue) ?? [] + return single ? Array(values.prefix(1)) : values +} + +/// How a selection is stored (`getStoredDefaultValue` / the change handlers). +private func storedSelection(_ values: [String], single: Bool) -> OpenUIValue { + if single { return values.first.map(OpenUIValue.string) ?? .undefined } + return .array(values.map(OpenUIValue.string)) +} + +/// Field plumbing shared by Chips (wrapping row) and OptionCards (card grid). +private struct SelectionField: View { + let props: ComponentProps + let componentType: String + var grid = false + @ViewBuilder let option: (ComponentProps, Bool, @escaping () -> Void) -> Option + @Environment(OpenUIContext.self) private var context + @Environment(\.openUIFormName) private var form + @Environment(FormValidation.self) private var validation: FormValidation? + + var body: some View { + let single = props.string("type") == "single" + let field = context.stateField(name: props.text("name"), binding: props["value"], form: form) + let rules = parseStructuredRules(props["rules"]) + let selected = selection( + stored: field.value, defaultValue: props["defaultValue"], single: single) + let items = props.children("items") + Group { + if grid { + ResponsiveCardGrid(count: items.count, maxPerRow: 3) { index in + optionView(items[index], selected: selected, single: single, field: field, rules: rules) + } + } else { + FlowLayout(spacing: 8) { + ForEach(items.indices, id: \.self) { index in + optionView(items[index], selected: selected, single: single, field: field, rules: rules) + } + } + } + } + .disabled(context.isStreaming) + .formField(field.name, rules: rules, value: field.value) + .onAppear { seedDefault(single: single, field: field) } + .onChange(of: context.isStreaming) { seedDefault(single: single, field: field) } + } + + private func optionView( + _ item: ComponentProps, selected: [String], single: Bool, field: StateField, + rules: [ParsedRule] + ) -> some View { + let value = item.text("value") + return option(item, selected.contains(value)) { + var next = selected + if single { + next = selected == [value] ? [] : [value] + } else if let index = next.firstIndex(of: value) { + next.remove(at: index) + } else { + next.append(value) + } + let stored = storedSelection(next, single: single) + field.setValue(stored) + if !rules.isEmpty { + validation?.validateField(field.name, value: stored, rules: rules) + } + } + .disabled(item.bool("disabled") == true) + } + + /// Stores the default once streaming ends, without notifying the host. + private func seedDefault(single: Bool, field: StateField) { + guard !field.isReactive else { return } + let defaults = selection( + stored: .undefined, defaultValue: props["defaultValue"], single: single) + guard !defaults.isEmpty else { return } + context.setDefaultValue( + form: form, componentType: componentType, name: field.name, + value: storedSelection(defaults, single: single)) + } +} + +// MARK: - Chips + +struct ChipsView: View { + let props: ComponentProps + var body: some View { + SelectionField(props: props, componentType: "Chips") { item, isOn, toggle in + Chip(item: item, isOn: isOn, action: toggle) + } + } +} + +struct ChipItemView: View { + let props: ComponentProps + var body: some View { Chip(item: props, isOn: false, action: {}) } +} + +private struct Chip: View { + let item: ComponentProps + let isOn: Bool + let action: () -> Void + @Environment(\.openUITheme) private var theme + + var body: some View { + Button(action: action) { + Hovering { hovered in + HStack(spacing: 4) { + if let symbol = iconSymbol(item["icon"]) { Image(systemName: symbol) } + Text(item.text("label")) + } + .font(.subheadline) + .padding(.horizontal, 8) + .padding(.vertical, 6) + .background( + isOn ? Color.primary.opacity(0.08) : hovered ? theme.subtleSurface : .clear, + in: RoundedRectangle(cornerRadius: 8) + ) + .overlay( + RoundedRectangle(cornerRadius: 8).strokeBorder( + isOn ? .primary : hovered ? theme.border : theme.interactiveBorder) + ) + .contentShape(RoundedRectangle(cornerRadius: 8)) + } + } + .buttonStyle(.openUIHover) + .accessibilityAddTraits(isOn ? .isSelected : []) + } +} + +// MARK: - Option cards + +struct OptionCardsView: View { + let props: ComponentProps + var body: some View { + SelectionField(props: props, componentType: "OptionCards", grid: true) { item, isOn, toggle in + OptionCardTile(item: item, isOn: isOn, action: toggle) + } + } +} + +struct OptionCardView: View { + let props: ComponentProps + var body: some View { OptionCardTile(item: props, isOn: false, action: {}) } +} + +private struct OptionCardTile: View { + let item: ComponentProps + let isOn: Bool + let action: () -> Void + @Environment(\.openUITheme) private var theme + + var body: some View { + Button(action: action) { + Hovering { hovered in + VStack(alignment: .leading, spacing: 6) { + switch item["topContent"] { + case .element(let element) where element.typeName == "Icon": + Image(systemName: iconSymbol(item["topContent"]) ?? "circle") + .font(.system(size: 14)) + .frame(width: 32, height: 32) + .background(theme.sunkSurface, in: RoundedRectangle(cornerRadius: 8)) + case .element: + OpenUINode(item["topContent"]).frame(height: 90).clipped() + default: + EmptyView() + } + Text(item.text("title")).font(.subheadline.weight(.semibold)) + if let subtitle = item.string("subtitle"), !subtitle.isEmpty { + Text(subtitle).font(.caption).foregroundStyle(.secondary) + } + } + .padding(10) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + .background(isOn ? theme.sunkSurface : .clear, in: RoundedRectangle(cornerRadius: 14)) + .overlay( + RoundedRectangle(cornerRadius: 14).strokeBorder( + // react-ui darkens an unselected card's border under the pointer. + isOn ? .primary : hovered ? Color.primary.opacity(0.3) : theme.interactiveBorder) + ) + .contentShape(RoundedRectangle(cornerRadius: 14)) + } + } + .buttonStyle(.openUIHover) + .accessibilityAddTraits(isOn ? .isSelected : []) + } +} + +// MARK: - Editable table + +/// A spreadsheet-like table. Edits stay local until Save, which stores the rows +/// as `{ id, values }` under the table's name and sends "Save Changes", as in +/// react-ui. +struct EditableTableView: View { + let props: ComponentProps + @State private var edits = TableEdits() + @FocusState private var focus: Cell? + @Environment(OpenUIContext.self) private var context + @Environment(\.openUITheme) private var theme + + var body: some View { + let columns = props.array("columns") + let rows = props.array("data") + let name = props.string("name") ?? "editable-table" + let changed = edits.count + VStack(alignment: .leading, spacing: theme.compactSpacing) { + ScrollView(.horizontal, showsIndicators: false) { + Grid(alignment: .leading, horizontalSpacing: 10, verticalSpacing: 6) { + GridRow { + ForEach(Array(columns.enumerated()), id: \.offset) { _, column in + Text(displayText(column["header"].isNullish ? column["key"] : column["header"])) + .font(.subheadline.weight(.semibold)) + } + } + Divider() + ForEach(Array(rows.enumerated()), id: \.offset) { rowIndex, row in + let id = displayText(row["id"]) + GridRow { + ForEach(Array(columns.enumerated()), id: \.offset) { index, column in + cell( + Cell(row: rowIndex, column: index), rowId: id, column: column, + value: row["values"].arrayValue?[safe: index], rowCount: rows.count) + } + } + } + } + } + if changed > 0 { + HStack { + Text("\(changed) unsaved change\(changed == 1 ? "" : "s")").font(.caption) + .foregroundStyle(.secondary) + Spacer() + Button("Reset") { edits = TableEdits() }.buttonStyle(.borderless) + Button("Save Changes") { + let saved = OpenUIValue.array( + rows.map { row in + let id = displayText(row["id"]) + let values = (0.. OpenUIValue in + if let edit = edits[id, index] { + let number = jsStringToNumber(edit) + let numeric = columns[index]["type"].stringValue == "number" && !number.isNaN + return numeric ? .number(number) : .string(edit) + } + return row["values"].arrayValue?[safe: index] ?? .null + } + return ["id": row["id"], "values": .array(values)] + }) + context.runtime.setFieldValue( + form: name, componentType: "EditableTable", name: name, value: saved) + context.triggerAction("Save Changes", form: name) + edits = TableEdits() + } + .modifier(ProminentButton()) + } + } + } + .disabled(context.isStreaming) + } + + /// A cell's place in the table, for keyboard focus. + private struct Cell: Hashable { + let row: Int + let column: Int + } + + /// Moves focus `rows` rows from `cell`, staying in its column. + private func move(_ rows: Int, from cell: Cell, rowCount: Int) -> KeyPress.Result { + let row = cell.row + rows + guard (0.. some View { + let index = position.column + let width = column["width"].numberValue.flatMap(\.finite).map { CGFloat(max($0, 40)) } ?? 120 + let original = displayText(value ?? .null) + let binding = Binding( + get: { edits[rowId, index] ?? original }, + set: { edits.set($0, row: rowId, column: index, original: original) }) + return Group { + if column["type"].stringValue == "select", let options = column["options"].arrayValue { + Picker("", selection: binding) { + ForEach(Array(options.enumerated()), id: \.offset) { _, option in + Text(displayText(option["label"])).tag(displayText(option["value"])) + } + } + .labelsHidden() + } else { + // react-ui's spreadsheet keys, for a hardware keyboard: Enter keeps + // the edit and moves down, Up and Down move between rows. Tab already + // moves across. + TextField("", text: binding) + .textFieldStyle(.roundedBorder) + .focused($focus, equals: position) + .onSubmit { _ = move(1, from: position, rowCount: rowCount) } + .onKeyPress(.upArrow) { move(-1, from: position, rowCount: rowCount) } + .onKeyPress(.downArrow) { move(1, from: position, rowCount: rowCount) } + } + } + .frame(width: width) + } +} + +/// An editable table's unsaved edits, by row id and column. Only text that +/// differs from the data is an edit: a focused field writes its text back +/// unchanged, which must not count as a change. +struct TableEdits: Equatable { + private var rows: [String: [Int: String]] = [:] + + /// How many cells have unsaved edits. + var count: Int { rows.values.reduce(0) { $0 + $1.count } } + + subscript(row: String, column: Int) -> String? { rows[row]?[column] } + + mutating func set(_ text: String, row: String, column: Int, original: String) { + rows[row, default: [:]][column] = text == original ? nil : text + if rows[row]?.isEmpty == true { rows[row] = nil } + } +} + +extension Array { + subscript(safe index: Int) -> Element? { indices.contains(index) ? self[index] : nil } +} diff --git a/packages/swift-lang/Sources/OpenUISwiftUI/Components/StackComponents.swift b/packages/swift-lang/Sources/OpenUISwiftUI/Components/StackComponents.swift new file mode 100644 index 0000000000..93b4b827c0 --- /dev/null +++ b/packages/swift-lang/Sources/OpenUISwiftUI/Components/StackComponents.swift @@ -0,0 +1,324 @@ +import OpenUILang +import SwiftUI + +// MARK: - Flex layout + +/// As much of a CSS flex container as react-ui's Stack and Card use: a row or +/// a column with a gap, cross-axis `align`, main-axis `justify` and `wrap`. +/// +/// In a row, views that can grow (a Card, or anything with an infinite max +/// width) act like react-ui's cards, `flex: 1` with `min-width: 0`: they start +/// from nothing and share the free space equally, so they never wrap; other +/// views keep their own width and shrink in proportion when the row is too +/// narrow. Columns place each view at the full width. +struct FlexLayout: Layout { + enum Direction { + case row, column + } + + var direction = Direction.column + var gap: CGFloat = 12 + /// `start`, `center`, `end`, `stretch` or `baseline`; nil is CSS's default, + /// `stretch`. + var align: String? + /// `start`, `center`, `end`, `between`, `around` or `evenly`. + var justify: String? + var wrap = false + + /// A view's starting size along the row and whether it grows. + struct Item: Equatable { + var basis: CGFloat + var flexible: Bool + } + + /// react-ui's spacing tokens for Stack's `gap`, "m" by default. + static func gap(_ name: String?) -> CGFloat { + switch name { + case "none": 0 + case "xs": 6 + case "s": 8 + case "l": 18 + case "xl": 24 + case "2xl": 36 + default: 12 + } + } + + func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { + let frames = arrange(proposal: proposal, subviews: subviews) + let width = proposal.width ?? frames.map(\.maxX).max() ?? 0 + return CGSize(width: width, height: frames.map(\.maxY).max() ?? 0) + } + + func placeSubviews( + in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout () + ) { + let frames = arrange( + proposal: ProposedViewSize(width: bounds.width, height: nil), subviews: subviews) + for (subview, frame) in zip(subviews, frames) { + subview.place( + at: CGPoint(x: bounds.minX + frame.minX, y: bounds.minY + frame.minY), + proposal: ProposedViewSize(frame.size)) + } + } + + /// Each subview's frame, relative to the container's top-leading corner. + /// Views with no size, like a closed Modal, get no gap around them: in + /// react-ui they render nothing. + private func arrange(proposal: ProposedViewSize, subviews: Subviews) -> [CGRect] { + let shown = subviews.indices.filter { subviews[$0].sizeThatFits(.unspecified) != .zero } + var frames = [CGRect](repeating: .zero, count: subviews.count) + let placed = layOut(proposal: proposal, subviews: shown.map { subviews[$0] }) + for (index, frame) in zip(shown, placed) { frames[index] = frame } + return frames + } + + private func layOut(proposal: ProposedViewSize, subviews: [LayoutSubview]) -> [CGRect] { + if direction == .column { return column(width: proposal.width, subviews: subviews) } + let items = subviews.map { subview in + let ideal = subview.sizeThatFits(.unspecified).width + let flexible = subview.sizeThatFits(ProposedViewSize(width: .infinity, height: nil)).width + .isInfinite + return Item(basis: flexible ? 0 : ideal, flexible: flexible) + } + let width = + proposal.width + ?? subviews.map { $0.sizeThatFits(.unspecified).width }.reduce(0, +) + + gap * CGFloat(max(subviews.count - 1, 0)) + // react-ui's Stack doesn't spread wrapped lines apart. + let justify = wrap && self.justify == "between" ? "start" : self.justify + var frames = [CGRect](repeating: .zero, count: subviews.count) + var y: CGFloat = 0 + for line in Self.lines(items, width: width, gap: gap, wrap: wrap) { + let (widths, offsets) = Self.distribute( + Array(items[line]), width: width, gap: gap, justify: justify) + let heights = zip(line, widths).map { index, width in + subviews[index].sizeThatFits(ProposedViewSize(width: width, height: nil)).height + } + let height = heights.max() ?? 0 + let baselines = zip(line, zip(widths, heights)).map { index, size in + subviews[index].dimensions(in: ProposedViewSize(width: size.0, height: size.1))[ + .firstTextBaseline] + } + let baseline = baselines.max() ?? 0 + for (position, index) in line.enumerated() { + let itemHeight = heights[position] + let stretched = align == nil || align == "stretch" + let offset: CGFloat = + switch align { + case "center": (height - itemHeight) / 2 + case "end": height - itemHeight + case "baseline": baseline - baselines[position] + default: 0 + } + frames[index] = CGRect( + x: offsets[position], y: y + offset, width: widths[position], + height: stretched ? height : itemHeight) + } + y += height + gap + } + return frames + } + + private func column(width proposed: CGFloat?, subviews: [LayoutSubview]) -> [CGRect] { + let width = proposed ?? subviews.map { $0.sizeThatFits(.unspecified).width }.max() ?? 0 + var y: CGFloat = 0 + return subviews.map { subview in + let size = subview.sizeThatFits(ProposedViewSize(width: width, height: nil)) + let itemWidth = align == nil || align == "stretch" ? width : min(size.width, width) + let x: CGFloat = + switch align { + case "center": (width - itemWidth) / 2 + case "end": width - itemWidth + default: 0 + } + defer { y += size.height + gap } + return CGRect(x: x, y: y, width: itemWidth, height: size.height) + } + } + + /// Splits items into lines. Without `wrap` it's one line; with it, items + /// move to a new line when their basis doesn't fit. Growing items start + /// from nothing, so they never move on their own. + static func lines(_ items: [Item], width: CGFloat, gap: CGFloat, wrap: Bool) -> [Range] { + guard wrap, !items.isEmpty else { return [items.indices] } + var lines: [Range] = [] + var start = 0 + var used: CGFloat = 0 + for (index, item) in items.enumerated() { + let needed = index == start ? item.basis : used + gap + item.basis + if index > start, needed > width { + lines.append(start.. (widths: [CGFloat], offsets: [CGFloat]) { + let count = CGFloat(items.count) + let fixed = items.filter { !$0.flexible }.map(\.basis).reduce(0, +) + let free = width - gap * max(count - 1, 0) - fixed + let growing = CGFloat(items.filter(\.flexible).count) + var widths = items.map(\.basis) + if free < 0, fixed > 0 { + widths = items.map { $0.flexible ? 0 : max($0.basis + free * $0.basis / fixed, 0) } + } else if growing > 0 { + widths = items.map { $0.flexible ? free / growing : $0.basis } + } + let space = growing > 0 ? 0 : max(free, 0) + let (lead, between): (CGFloat, CGFloat) = + switch justify { + case "center": (space / 2, 0) + case "end": (space, 0) + case "between": (0, count > 1 ? space / (count - 1) : 0) + case "around": (space / count / 2, space / count) + case "evenly": (space / (count + 1), space / (count + 1)) + default: (0, 0) + } + var x = lead + let offsets = widths.map { width in + defer { x += width + gap + between } + return x + } + return (widths, offsets) + } +} + +extension FlexLayout { + /// The layout a Stack or Card asks for with its flex props. + init(_ props: ComponentProps, defaultAlign: String? = nil) { + self.init( + direction: props.string("direction") == "row" ? .row : .column, + gap: Self.gap(props.string("gap")), align: props.string("align") ?? defaultAlign, + justify: props.string("justify"), wrap: props.bool("wrap") ?? false) + } +} + +/// A list of nodes laid out by `layout`, with OpenUINodes' streaming insertions. +private struct FlexNodes: View { + let layout: FlexLayout + let values: [OpenUIValue] + + var body: some View { + let items = NodeItem.list(values) + layout { + ForEach(items) { item in + OpenUINode(item.value).transition(.openUIInsertion) + } + } + .openUIAnimation(Motion.insertion, value: items.map(\.id)) + } +} + +// MARK: - Stack and Card + +/// openuiLibrary's Stack: a flex container, a column by default. +struct StackView: View { + let props: ComponentProps + + var body: some View { + FlexNodes(layout: FlexLayout(props), values: props.array("children")) + .frame(maxWidth: .infinity, alignment: .leading) + } +} + +/// openuiLibrary's Card: react-ui's card surface ("card", "sunk", or "clear" +/// with only side padding) around a Stack-like flex layout that stretches its +/// children by default. Cards always take the full width, and in a row they +/// share it. +struct FlexCardView: View { + let props: ComponentProps + @Environment(\.openUITheme) private var theme + + var body: some View { + let variant = props.string("variant") ?? "card" + FlexNodes(layout: FlexLayout(props, defaultAlign: "stretch"), values: props.array("children")) + .padding(.horizontal, variant == "clear" ? theme.cardPadding : 0) + .frame(maxWidth: .infinity, alignment: .leading) + .surface(variant) + } +} + +// MARK: - Modal + +/// openuiLibrary's Modal: a sheet that its `open` binding shows (set the bound +/// `$state` to true). Closing it with its button, Escape or a swipe writes +/// false back, as react-ui's X, Escape and backdrop do. +struct ModalView: View { + let props: ComponentProps + @Environment(OpenUIContext.self) private var context + @Environment(\.openUITheme) private var theme + + var body: some View { + let field = context.stateField(name: "open", binding: props["open"], form: nil) + let isOpen = field.value == true || field.value == "true" + Color.clear + .frame(width: 0, height: 0) + .accessibilityHidden(true) + .sheet( + isPresented: Binding( + get: { isOpen }, + set: { open in if !open { field.setValue(false) } }) + ) { + ModalSheet(props: props) + .environment(context) + .environment(\.openUITheme, theme) + } + } +} + +private struct ModalSheet: View { + let props: ComponentProps + @Environment(\.dismiss) private var dismiss + + var body: some View { + VStack(spacing: 0) { + HStack { + Text(props.text("title")).font(.headline) + Spacer() + Button { + dismiss() + } label: { + Image(systemName: "xmark") + .font(.body.weight(.medium)) + .foregroundStyle(.secondary) + .frame(width: 28, height: 28) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .keyboardShortcut(.cancelAction) + .accessibilityLabel("Close") + } + .padding(12) + Divider() + ScrollView { + OpenUINodes(props.array("children")) + .padding(12) + .frame(maxWidth: .infinity, alignment: .leading) + } + } + #if os(macOS) + .frame(width: Self.width(props.string("size"))) + .frame(minHeight: 200, idealHeight: 420) + #endif + } + + /// react-ui's sm, md and lg widths. + static func width(_ size: String?) -> CGFloat { + switch size { + case "sm": 400 + case "lg": 720 + default: 560 + } + } +} diff --git a/packages/swift-lang/Sources/OpenUISwiftUI/Exports.swift b/packages/swift-lang/Sources/OpenUISwiftUI/Exports.swift new file mode 100644 index 0000000000..2a8894f65b --- /dev/null +++ b/packages/swift-lang/Sources/OpenUISwiftUI/Exports.swift @@ -0,0 +1 @@ +@_exported import OpenUILang diff --git a/packages/swift-lang/Sources/OpenUISwiftUI/FormValidation.swift b/packages/swift-lang/Sources/OpenUISwiftUI/FormValidation.swift new file mode 100644 index 0000000000..490a0d8409 --- /dev/null +++ b/packages/swift-lang/Sources/OpenUISwiftUI/FormValidation.swift @@ -0,0 +1,103 @@ +import Observation +import OpenUILang +import SwiftUI + +/// Validation state for one `Form`: fields register their rules, primary +/// buttons validate the whole form before acting, and fields show their error. +/// Ports react-lang's `useCreateFormValidation`. +@MainActor +@Observable +public final class FormValidation { + public private(set) var errors: [String: String] = [:] + + private struct Registration { + var rules: [ParsedRule] + var value: @MainActor () -> OpenUIValue + } + + @ObservationIgnored private var fields: [String: Registration] = [:] + + public init() {} + + public func error(for name: String) -> String? { errors[name] } + + public func register( + _ name: String, rules: [ParsedRule], value: @escaping @MainActor () -> OpenUIValue + ) { + fields[name] = Registration(rules: rules, value: value) + } + + public func unregister(_ name: String) { + fields[name] = nil + } + + /// Validates one field, storing or clearing its error. Returns whether it is valid. + @discardableResult + public func validateField(_ name: String, value: OpenUIValue, rules: [ParsedRule]) -> Bool { + let error = validate(value, rules) + if errors[name] != error { errors[name] = error } + return error == nil + } + + /// Validates every registered field. Returns whether the form is valid. + public func validateForm() -> Bool { + var next: [String: String] = [:] + for (name, field) in fields { + if let error = validate(unwrapFieldValue(field.value()), field.rules) { next[name] = error } + } + errors = next + return next.isEmpty + } + + public func clearError(_ name: String) { + if errors[name] != nil { errors[name] = nil } + } +} + +/// Form fields store `{ value, componentType }`; validation sees the raw value. +private func unwrapFieldValue(_ value: OpenUIValue) -> OpenUIValue { + if case .object(let object) = value, object.contains("componentType"), let inner = object["value"] + { + return inner + } + return value +} + +/// Registers a field's rules with the enclosing form while it is on screen. +struct FormFieldRegistration: ViewModifier { + let name: String + let rules: [ParsedRule] + let value: OpenUIValue + @Environment(FormValidation.self) private var validation: FormValidation? + @Environment(OpenUIContext.self) private var context + + func body(content: Content) -> some View { + content + .onChange( + of: RegistrationKey(name: name, rules: rules, isStreaming: context.isStreaming), + initial: true + ) { + guard let validation, !rules.isEmpty, !context.isStreaming else { return } + let current = value + validation.register(name, rules: rules) { current } + } + .onChange(of: value) { + guard let validation, !rules.isEmpty, !context.isStreaming else { return } + let current = value + validation.register(name, rules: rules) { current } + } + .onDisappear { validation?.unregister(name) } + } + + private struct RegistrationKey: Equatable { + let name: String + let rules: [ParsedRule] + let isStreaming: Bool + } +} + +extension View { + func formField(_ name: String, rules: [ParsedRule], value: OpenUIValue) -> some View { + modifier(FormFieldRegistration(name: name, rules: rules, value: value)) + } +} diff --git a/packages/swift-lang/Sources/OpenUISwiftUI/Hover.swift b/packages/swift-lang/Sources/OpenUISwiftUI/Hover.swift new file mode 100644 index 0000000000..03fc4c2052 --- /dev/null +++ b/packages/swift-lang/Sources/OpenUISwiftUI/Hover.swift @@ -0,0 +1,63 @@ +import SwiftUI + +private struct HoveredKey: EnvironmentKey { + static let defaultValue = false +} + +extension EnvironmentValues { + /// Whether a pointer is over the button this view is the label of (on the + /// Mac, and on iPad with a pointer; never for touch). Labels use it for + /// react-ui's hover state, which differs per control: chips fill in, option + /// cards' borders darken, list markers grow. + var openUIHovered: Bool { + get { self[HoveredKey.self] } + set { self[HoveredKey.self] = newValue } + } +} + +/// A plain button that tells its label when the pointer is over it, through +/// `openUIHovered`. With `sinks`, it's react-ui's clickable card: it sinks a +/// little under the pointer (scale 0.995) and more while pressed; otherwise it +/// dims while pressed, as the plain style does. +struct HoverButtonStyle: ButtonStyle { + var sinks = false + + func makeBody(configuration: Configuration) -> some View { + HoverLabel(configuration: configuration, sinks: sinks) + } + + private struct HoverLabel: View { + let configuration: Configuration + let sinks: Bool + @State private var hovering = false + @Environment(\.isEnabled) private var isEnabled + @Environment(\.accessibilityReduceMotion) private var reduceMotion + + var body: some View { + let hovered = hovering && isEnabled + let pressed = configuration.isPressed + configuration.label + .environment(\.openUIHovered, hovered) + .scaleEffect(sinks && !reduceMotion ? (pressed ? 0.98 : hovered ? 0.995 : 1) : 1) + .opacity(!sinks && pressed ? 0.6 : 1) + .onHover { hovering = $0 } + .animation(.easeOut(duration: 0.15), value: hovered) + .animation(.easeOut(duration: 0.1), value: pressed) + } + } +} + +extension ButtonStyle where Self == HoverButtonStyle { + /// A plain button whose label sees pointer hover (`openUIHovered`). + static var openUIHover: HoverButtonStyle { HoverButtonStyle() } + /// react-ui's clickable card: sinks a little under the pointer. + static var openUICard: HoverButtonStyle { HoverButtonStyle(sinks: true) } +} + +/// Reads `openUIHovered` for a label built inline in a button. +struct Hovering: View { + @ViewBuilder let content: (Bool) -> Content + @Environment(\.openUIHovered) private var hovered + + var body: some View { content(hovered) } +} diff --git a/packages/swift-lang/Sources/OpenUISwiftUI/Motion.swift b/packages/swift-lang/Sources/OpenUISwiftUI/Motion.swift new file mode 100644 index 0000000000..0563d03ce1 --- /dev/null +++ b/packages/swift-lang/Sources/OpenUISwiftUI/Motion.swift @@ -0,0 +1,130 @@ +import Charts +import SwiftUI + +/// The motion react-ui uses while a response streams in, so both renderers +/// feel the same. Everything here is skipped when Reduce Motion is on. +enum Motion { + /// react-ui's `dataMorph` spring (stiffness 480, damping 44): chart values + /// glide to their new positions as more data streams in. + static let dataMorph = Animation.interpolatingSpring(mass: 1, stiffness: 480, damping: 44) + + /// How a newly streamed part of the response appears. + static let insertion = Animation.easeOut(duration: 0.25) + + /// A tab, accordion or section changing what it shows. + static let reveal = Animation.easeInOut(duration: 0.2) +} + +extension AnyTransition { + /// Newly streamed nodes fade in and settle up a few points; removed ones fade out. + static var openUIInsertion: AnyTransition { + .asymmetric(insertion: .opacity.combined(with: .offset(y: 6)), removal: .opacity) + } +} + +extension View { + /// Animates changes to `value` unless Reduce Motion is on. + func openUIAnimation(_ animation: Animation, value: V) -> some View { + modifier(MotionAnimation(animation: animation, value: value)) + } +} + +private struct MotionAnimation: ViewModifier { + let animation: Animation + let value: V + @Environment(\.accessibilityReduceMotion) private var reduceMotion + + func body(content: Content) -> some View { + content.animation(reduceMotion ? nil : animation, value: value) + } +} + +/// Runs `change` with `animation`, or without one when Reduce Motion is on. +@MainActor +func withOpenUIAnimation(_ animation: Animation, _ change: () -> Void) { + #if canImport(UIKit) + let reduceMotion = UIAccessibility.isReduceMotionEnabled + #else + let reduceMotion = NSWorkspace.shared.accessibilityDisplayShouldReduceMotion + #endif + if reduceMotion { change() } else { withAnimation(animation, change) } +} + +/// A pulsing placeholder, like react-ui's skeleton bar: 30% to 85% opacity +/// and back every 1.2 seconds. +struct SkeletonBlock: View { + var height: CGFloat + var cornerRadius: CGFloat = 6 + @State private var bright = false + @Environment(\.accessibilityReduceMotion) private var reduceMotion + + var body: some View { + RoundedRectangle(cornerRadius: cornerRadius) + .fill(Color.primary.opacity(0.08)) + .frame(maxWidth: .infinity) + .frame(height: height) + .opacity(reduceMotion ? 0.6 : (bright ? 0.85 : 0.3)) + .onAppear { + guard !reduceMotion else { return } + withAnimation(.easeInOut(duration: 0.6).repeatForever(autoreverses: true)) { bright = true } + } + .accessibilityHidden(true) + } +} + +/// A chart's entrance, like react-ui's: bars grow up from the axis over 0.6s, +/// lines and areas draw from left to right over 1s. +struct ChartEntrance: ViewModifier { + enum Style { + case grow, growSideways, draw + } + + let style: Style + @State private var progress: CGFloat = 0 + @Environment(\.accessibilityReduceMotion) private var reduceMotion + + func body(content: Content) -> some View { + content + .chartPlotStyle { plot in + plot.mask { + Rectangle().scaleEffect( + x: style == .grow ? 1 : progress, y: style == .grow ? progress : 1, + anchor: style == .grow ? .bottom : .leading) + } + } + .onAppear { + guard progress == 0 else { return } + if reduceMotion { + progress = 1 + } else { + withAnimation(.easeOut(duration: style == .draw ? 1 : 0.6)) { progress = 1 } + } + } + } +} + +extension View { + func chartEntrance(_ style: ChartEntrance.Style) -> some View { + modifier(ChartEntrance(style: style)) + } +} + +/// react-ui's fade-appear entrance (0.6s) for charts that don't grow or draw: +/// pie, radial, radar and scatter. +private struct FadeAppear: ViewModifier { + @State private var visible = false + @Environment(\.accessibilityReduceMotion) private var reduceMotion + + func body(content: Content) -> some View { + content + .opacity(visible || reduceMotion ? 1 : 0) + .onAppear { + guard !visible else { return } + withAnimation(.easeOut(duration: 0.6)) { visible = true } + } + } +} + +extension View { + func fadeAppear() -> some View { modifier(FadeAppear()) } +} diff --git a/packages/swift-lang/Sources/OpenUISwiftUI/OpenUIChatLibrary.swift b/packages/swift-lang/Sources/OpenUISwiftUI/OpenUIChatLibrary.swift new file mode 100644 index 0000000000..b17bc40fc6 --- /dev/null +++ b/packages/swift-lang/Sources/OpenUISwiftUI/OpenUIChatLibrary.swift @@ -0,0 +1,116 @@ +import OpenUILang +import SwiftUI + +/// The OpenUI chat library rendered with native SwiftUI views. Component names +/// and arguments match `openuiChatLibrary` from @openuidev/react-ui (see +/// `ChatComponents`), and the root is `Card`. +public enum OpenUIChatLibrary { + @MainActor + public static let library = SwiftUILibrary( + components: ChatComponents.all.map { schema in + SwiftUIComponent(schema, content: views[schema.name] ?? { _ in AnyView(EmptyView()) }) + }, + root: "Card", + componentGroups: ChatComponents.groups) + + /// Sub-components that only carry data for their parent (a chart reads its + /// `Series`); they render nothing on their own. + static let dataOnly: Set = ["Series", "ScatterSeries", "Point", "Slice"] + + @MainActor + static let views: [String: ComponentContent] = { + func make(_ view: @escaping @MainActor (ComponentProps) -> V) -> ComponentContent { + { AnyView(view($0)) } + } + return [ + // Content + "Card": make(CardView.init), + "CardHeader": make(CardHeaderView.init), + "TextContent": make(TextContentView.init), + "MarkDownRenderer": make(MarkDownRendererView.init), + "Callout": make(CalloutView.init), + "TextCallout": make(TextCalloutView.init), + "CodeBlock": make(CodeBlockView.init), + "Image": make(ImageView.init), + "ImageBlock": make(ImageBlockView.init), + "ImageGallery": make(ImageGalleryView.init), + "Separator": make(SeparatorView.init), + "InlineHeader": make(InlineHeaderView.init), + // Tables + "Table": make(TableView.init), + "Col": make(ColView.init), + "EditableTable": make(EditableTableView.init), + // Charts + "BarChart": make(BarChartView.init), + "LineChart": make(LineChartView.init), + "AreaChart": make(AreaChartView.init), + "RadarChart": make(RadarChartView.init), + "HorizontalBarChart": make(HorizontalBarChartView.init), + "PieChart": make(PieChartView.init), + "RadialChart": make(RadialChartView.init), + "SingleStackedBarChart": make(SingleStackedBarChartView.init), + "ScatterChart": make(ScatterChartView.init), + // Forms + "Form": make(FormView.init), + "FormControl": make(FormControlView.init), + "Label": make(LabelView.init), + "Input": make(InputView.init), + "TextArea": make(TextAreaView.init), + "Select": make(SelectView.init), + "SelectItem": make(SelectItemView.init), + "DatePicker": make(DatePickerView.init), + "Slider": make(SliderView.init), + "CheckBoxGroup": make(CheckBoxGroupView.init), + "CheckBoxItem": make(CheckBoxItemView.init), + "RadioGroup": make(RadioGroupView.init), + "RadioItem": make(RadioItemView.init), + "SwitchGroup": make(SwitchGroupView.init), + "SwitchItem": make(SwitchItemView.init), + "Chips": make(ChipsView.init), + "ChipItem": make(ChipItemView.init), + "OptionCards": make(OptionCardsView.init), + "OptionCard": make(OptionCardView.init), + // Buttons and icons + "Button": make(ButtonView.init), + "Buttons": make(ButtonsView.init), + "Icon": make(IconView.init), + "IconButton": make(IconButtonView.init), + // Lists and layout + "ListBlock": make(ListBlockView.init), + "ListItem": make(ListItemView.init), + "FollowUpBlock": make(FollowUpBlockView.init), + "FollowUpItem": make(FollowUpItemView.init), + "SectionBlock": make(SectionBlockView.init), + "SectionItem": make(TriggeredContentView.init), + "Tabs": make(TabsView.init), + "TabItem": make(TriggeredContentView.init), + "Accordion": make(AccordionView.init), + "AccordionItem": make(TriggeredContentView.init), + "Steps": make(StepsView.init), + "StepsItem": make(StepsItemView.init), + "Carousel": make(CarouselView.init), + // Data display + "TagBlock": make(TagBlockView.init), + "Tag": make(TagView.init), + "EntityList": make(EntityListView.init), + // Cards and their building blocks + "SnippetCardBlock": make(SnippetCardBlockView.init), + "SnippetCardItem": make(SnippetCardItemView.init), + "OverviewCardBlock": make(OverviewCardBlockView.init), + "OverviewCardItem": make(OverviewCardItemView.init), + "ContextCardBlock": make(ContextCardBlockView.init), + "ContextCardItem": make(ContextCardItemView.init), + "CompositeCardBlock": make(CompositeCardBlockView.init), + "CompositeCardItem": make(CompositeCardItemView.init), + "VisualCardBlock": make(VisualCardBlockView.init), + "VisualCardItem": make(VisualCardItemView.init), + "Text": make { TextLineView(props: $0, bold: false) }, + "BoldText": make { TextLineView(props: $0, bold: true) }, + "IconText": make(IconTextView.init), + "ImageText": make(ImageTextView.init), + "ImageTextLarge": make(ImageTextLargeView.init), + "MetricIndicatorInline": make(MetricIndicatorInlineView.init), + "MetricIndicatorWithStrikethrough": make(MetricIndicatorWithStrikethroughView.init), + ] + }() +} diff --git a/packages/swift-lang/Sources/OpenUISwiftUI/OpenUIContext.swift b/packages/swift-lang/Sources/OpenUISwiftUI/OpenUIContext.swift new file mode 100644 index 0000000000..260b5e1c33 --- /dev/null +++ b/packages/swift-lang/Sources/OpenUISwiftUI/OpenUIContext.swift @@ -0,0 +1,133 @@ +import Observation +import OpenUILang +import SwiftUI + +/// How a component renders: it receives its evaluated props and returns a view. +public typealias ComponentContent = @MainActor (ComponentProps) -> AnyView + +/// A SwiftUI component library. +public typealias SwiftUILibrary = Library + +/// A component definition rendered with SwiftUI. +public typealias SwiftUIComponent = ComponentDefinition + +extension ComponentDefinition where Content == ComponentContent { + /// Pairs a schema with the SwiftUI view that renders it. + public init( + _ schema: ComponentSchema, @ViewBuilder view: @escaping @MainActor (ComponentProps) -> V + ) { + self.init(schema, content: { AnyView(view($0)) }) + } +} + +/// The renderer state shared with every component through the environment: +/// the runtime, streaming status, form fields and actions. Observing it +/// re-renders the tree when state or query results change. +@MainActor +@Observable +public final class OpenUIContext { + public let runtime: OpenUIRuntime + public let library: SwiftUILibrary + + /// Bumped whenever the response, `$state` or query results change. + public private(set) var revision = 0 + + /// The chart whose tooltip is open. A tapped tooltip stays open until it's + /// tapped again, so opening another one closes it. + var chartWithTooltip: UUID? + + @ObservationIgnored var onAction: ((ActionEvent) -> Void)? + @ObservationIgnored var onStateUpdate: ((OpenUIObject) -> Void)? + @ObservationIgnored var onParseResult: ((ParseResult?) -> Void)? + @ObservationIgnored var onError: (([OpenUIError]) -> Void)? + @ObservationIgnored private var unsubscribers: [() -> Void] = [] + + /// - Parameter response: The response to show on the first render, before + /// ``update(response:isStreaming:)`` is called. + public init( + library: SwiftUILibrary, initialState: OpenUIObject? = nil, + toolProvider: (any ToolProvider)? = nil, publishObservability: Bool = true, + response: String? = nil, isStreaming: Bool = false + ) { + self.library = library + self.runtime = OpenUIRuntime( + library: library, initialState: initialState, toolProvider: toolProvider) + runtime.onAction = { [weak self] event in self?.onAction?(event) } + runtime.onStateUpdate = { [weak self] state in self?.onStateUpdate?(state) } + runtime.onParseResult = { [weak self] result in self?.onParseResult?(result) } + runtime.onError = { [weak self] errors in self?.onError?(errors) } + unsubscribers.append(runtime.store.subscribe { [weak self] in self?.revision += 1 }) + unsubscribers.append(runtime.queries.subscribe { [weak self] in self?.revision += 1 }) + runtime.publishesObservability = publishObservability + runtime.preload(response: response, isStreaming: isStreaming) + } + + /// Feeds the latest response text. + public func update(response: String?, isStreaming: Bool) { + runtime.update(response: response, isStreaming: isStreaming) + revision += 1 + } + + /// The evaluated root element for the current state. + public var root: ElementNode? { + _ = revision + return runtime.evaluatedRoot() + } + + public var isStreaming: Bool { + _ = revision + return runtime.isStreaming + } + + public var isQueryLoading: Bool { + _ = revision + return runtime.queries.isAnyLoading + } + + /// Runs a component's action (see `OpenUIRuntime.triggerAction`). + public func triggerAction(_ label: String, form: String? = nil, action: OpenUIValue? = nil) { + Task { await runtime.triggerAction(label, form: form, action: action) } + } + + /// The form field or `$binding` a component reads and writes. + public func stateField(name: String, binding: OpenUIValue, form: String?) -> StateField { + _ = revision + return runtime.stateField(name: name, binding: binding, form: form) + } + + public func fieldValue(form: String?, name: String) -> OpenUIValue { + _ = revision + return runtime.fieldValue(form: form, name: name) + } + + /// Seeds a field's default once streaming ends, without notifying the host + /// (react-lang's `useSetDefaultValue`). + public func setDefaultValue( + form: String?, componentType: String, name: String, value: OpenUIValue + ) { + guard !runtime.isStreaming, !value.isNullish, + runtime.fieldValue(form: form, name: name) == .undefined + else { return } + runtime.setFieldValue( + form: form, componentType: componentType, name: name, value: value, notifyHost: false) + } + + /// Stops query refreshes and drops subscriptions. + public func dispose() { + for unsubscribe in unsubscribers { unsubscribe() } + unsubscribers.removeAll() + runtime.dispose() + } +} + +private struct FormNameKey: EnvironmentKey { + static let defaultValue: String? = nil +} + +extension EnvironmentValues { + /// The enclosing form's name; form fields store their values under it. + public var openUIFormName: String? { + get { self[FormNameKey.self] } + set { self[FormNameKey.self] = newValue } + } +} diff --git a/packages/swift-lang/Sources/OpenUISwiftUI/OpenUILibrary.swift b/packages/swift-lang/Sources/OpenUISwiftUI/OpenUILibrary.swift new file mode 100644 index 0000000000..f3bab350c0 --- /dev/null +++ b/packages/swift-lang/Sources/OpenUISwiftUI/OpenUILibrary.swift @@ -0,0 +1,29 @@ +import OpenUILang +import SwiftUI + +/// react-ui's general OpenUI library rendered with native SwiftUI views. +/// Component names and arguments match `openuiLibrary` from +/// @openuidev/react-ui (see `OpenUIComponents`), and the root is `Stack`. +/// +/// It shares the chat library's views; `Stack`, `Card` (with Stack's layout +/// props) and `Modal` are its own. +public enum OpenUILibrary { + @MainActor + public static let library = SwiftUILibrary( + components: OpenUIComponents.all.map { schema in + SwiftUIComponent( + schema, + content: views[schema.name] ?? OpenUIChatLibrary.views[schema.name] ?? { _ in + AnyView(EmptyView()) + }) + }, + root: "Stack", + componentGroups: OpenUIComponents.groups) + + @MainActor + static let views: [String: ComponentContent] = [ + "Stack": { AnyView(StackView(props: $0)) }, + "Card": { AnyView(FlexCardView(props: $0)) }, + "Modal": { AnyView(ModalView(props: $0)) }, + ] +} diff --git a/packages/swift-lang/Sources/OpenUISwiftUI/OpenUINode.swift b/packages/swift-lang/Sources/OpenUISwiftUI/OpenUINode.swift new file mode 100644 index 0000000000..2b0c64e79c --- /dev/null +++ b/packages/swift-lang/Sources/OpenUISwiftUI/OpenUINode.swift @@ -0,0 +1,93 @@ +import OpenUILang +import SwiftUI + +/// Renders any prop value: an element through its library component, an array +/// as a stack of nodes, scalars as text, and nothing for null. +public struct OpenUINode: View { + public let value: OpenUIValue + + public init(_ value: OpenUIValue) { + self.value = value + } + + public var body: some View { + switch value { + case .element(let element): + OpenUIElementView(element: element) + case .array(let items): + OpenUINodes(items) + case .string, .number, .bool: + Text(displayText(value)) + default: + EmptyView() + } + } +} + +/// Renders a list of nodes in a vertical stack. Each node keeps its identity +/// across streamed updates (by statement name, else position), so inputs keep +/// focus and local state while the response re-renders. +public struct OpenUINodes: View { + let items: [NodeItem] + let spacing: CGFloat? + let alignment: HorizontalAlignment + + public init( + _ values: [OpenUIValue], spacing: CGFloat? = OpenUITheme.default.spacing, + alignment: HorizontalAlignment = .leading + ) { + self.items = NodeItem.list(values) + self.spacing = spacing + self.alignment = alignment + } + + public var body: some View { + VStack(alignment: alignment, spacing: spacing) { + ForEach(items) { item in + OpenUINode(item.value) + // Each node is as tall as it needs to be at this width. Offered + // exactly the stack's ideal height (a fixed frame, a self-sizing + // cell), a VStack splits it by flexibility instead, so wrapping text + // next to a row with a Spacer is cut short and space is left over. + .fixedSize(horizontal: false, vertical: true) + .transition(.openUIInsertion) + } + } + // Nodes keep their identity, so only newly streamed ones animate in. + .openUIAnimation(Motion.insertion, value: items.map(\.id)) + } +} + +/// A node with a stable identity for `ForEach`. +struct NodeItem: Identifiable { + let id: String + let value: OpenUIValue + + static func list(_ values: [OpenUIValue]) -> [NodeItem] { + var seen: [String: Int] = [:] + return values.enumerated().compactMap { index, value in + if value.isNullish { return nil } + var id = value.elementValue?.statementId ?? "#\(index)" + // A statement referenced twice in one list still needs unique ids. + if let count = seen[id] { + seen[id] = count + 1 + id += "~\(count)" + } else { + seen[id] = 1 + } + return NodeItem(id: id, value: value) + } + } +} + +/// Renders an element with its library component; unknown components render nothing. +struct OpenUIElementView: View { + let element: ElementNode + @Environment(OpenUIContext.self) private var context + + var body: some View { + if let component = context.library.component(named: element.typeName) { + component.content(ComponentProps(element)) + } + } +} diff --git a/packages/swift-lang/Sources/OpenUISwiftUI/OpenUIRenderer.swift b/packages/swift-lang/Sources/OpenUISwiftUI/OpenUIRenderer.swift new file mode 100644 index 0000000000..973de35d17 --- /dev/null +++ b/packages/swift-lang/Sources/OpenUISwiftUI/OpenUIRenderer.swift @@ -0,0 +1,138 @@ +import OpenUILang +import SwiftUI + +/// Renders an OpenUI Lang response with a component library, updating as the +/// response streams in. +/// +/// ```swift +/// OpenUIRenderer(response: text, isStreaming: isStreaming, library: OpenUIChatLibrary.library) { +/// event in send(event.humanFriendlyMessage) +/// } +/// ``` +public struct OpenUIRenderer: View { + let response: String? + let isStreaming: Bool + let queryLoader: QueryLoader + let onAction: ((ActionEvent) -> Void)? + let onStateUpdate: ((OpenUIObject) -> Void)? + let onParseResult: ((ParseResult?) -> Void)? + let onError: (([OpenUIError]) -> Void)? + + // A StateObject because its initializer is an autoclosure: the context (and + // the runtime behind it) is created once per view, not each time a parent + // redraws and builds this struct again. + @StateObject private var holder: ContextHolder + private var context: OpenUIContext { holder.context } + @Environment(\.openUITheme) private var theme + + /// - Parameters: + /// - response: The response text so far. + /// - isStreaming: Whether more text is still arriving. + /// - library: The components the response may use. Changing it requires a new view identity. + /// - initialState: Restored `$state` and form values, e.g. from a saved conversation. + /// - toolProvider: Handles `Query` and `Mutation` tool calls. + /// - publishObservability: Whether to publish stream events on `Observability.shared`. + /// - queryLoader: Shown in the top trailing corner while queries load. + /// - onAction: Called for actions the host handles (continue the conversation, open a URL). + /// A trailing closure binds here. + /// - onStateUpdate: Called when `$state` or form values change. + /// - onParseResult: Called whenever the parse result changes. + /// - onError: Called with errors an LLM can fix once the response finishes, and with `[]` + /// when they clear. See `OpenUIRuntime.onError`. + public init( + response: String?, isStreaming: Bool = false, library: SwiftUILibrary, + initialState: OpenUIObject? = nil, toolProvider: (any ToolProvider)? = nil, + publishObservability: Bool = true, queryLoader: QueryLoader, + onAction: ((ActionEvent) -> Void)? = nil, + onStateUpdate: ((OpenUIObject) -> Void)? = nil, + onParseResult: ((ParseResult?) -> Void)? = nil, + onError: (([OpenUIError]) -> Void)? = nil + ) { + self.response = response + self.isStreaming = isStreaming + self.queryLoader = queryLoader + self.onAction = onAction + self.onStateUpdate = onStateUpdate + self.onParseResult = onParseResult + self.onError = onError + // The context parses the response up front, so the first frame shows it. + // Without that, a renderer in a lazy stack is rebuilt empty each time it + // scrolls back into view, then grows, and the stack keeps re-placing rows. + _holder = StateObject( + wrappedValue: ContextHolder( + OpenUIContext( + library: library, initialState: initialState, toolProvider: toolProvider, + publishObservability: publishObservability, response: response, + isStreaming: isStreaming))) + } + + public var body: some View { + let _ = syncHandlers() + // Not a Group: modifiers on a Group apply to its children, and before the + // first update there are none, so a nested renderer whose response never + // changes (a restored message) would never run the onChange below. + VStack(alignment: .leading, spacing: 0) { + if let root = context.root { + OpenUIElementView(element: root) + .transition(.openUIInsertion) + .opacity(context.isQueryLoading ? 0.7 : 1) + .animation(.easeInOut(duration: 0.2), value: context.isQueryLoading) + .overlay(alignment: .topTrailing) { + if context.isQueryLoading { queryLoader.padding(8) } + } + } + } + .environment(context) + .tint(theme.accent) + .onChange(of: response, initial: true) { + context.update(response: response, isStreaming: isStreaming) + } + .onChange(of: isStreaming) { + context.update(response: response, isStreaming: isStreaming) + } + } + + /// Hands the latest closures to the context. They aren't observed, so this + /// never triggers another render. + private func syncHandlers() -> Bool { + context.onAction = onAction + context.onStateUpdate = onStateUpdate + context.onParseResult = onParseResult + context.onError = onError + return true + } +} + +extension OpenUIRenderer where QueryLoader == DefaultQueryLoader { + /// A renderer with the default query loading indicator. + public init( + response: String?, isStreaming: Bool = false, library: SwiftUILibrary, + initialState: OpenUIObject? = nil, toolProvider: (any ToolProvider)? = nil, + publishObservability: Bool = true, onAction: ((ActionEvent) -> Void)? = nil, + onStateUpdate: ((OpenUIObject) -> Void)? = nil, + onParseResult: ((ParseResult?) -> Void)? = nil, + onError: (([OpenUIError]) -> Void)? = nil + ) { + self.init( + response: response, isStreaming: isStreaming, library: library, initialState: initialState, + toolProvider: toolProvider, publishObservability: publishObservability, + queryLoader: DefaultQueryLoader(), onAction: onAction, + onStateUpdate: onStateUpdate, onParseResult: onParseResult, onError: onError) + } +} + +/// The small spinner shown while queries load. +public struct DefaultQueryLoader: View { + public init() {} + + public var body: some View { + ProgressView().controlSize(.small) + } +} + +/// Keeps a renderer's context for the life of the view. +@MainActor +private final class ContextHolder: ObservableObject { + let context: OpenUIContext + init(_ context: OpenUIContext) { self.context = context } +} diff --git a/packages/swift-lang/Sources/OpenUISwiftUI/Support.swift b/packages/swift-lang/Sources/OpenUISwiftUI/Support.swift new file mode 100644 index 0000000000..654aab2c45 --- /dev/null +++ b/packages/swift-lang/Sources/OpenUISwiftUI/Support.swift @@ -0,0 +1,399 @@ +import OpenUILang +import SwiftUI + +/// Text with inline markdown (bold, italic, code, links), falling back to the +/// raw string when it doesn't parse (e.g. half-streamed markup). +struct InlineMarkdown: View { + let source: String + + init(_ source: String) { + self.source = source + } + + var body: some View { + Text(Self.attributed(source)) + } + + static func attributed(_ source: String) -> AttributedString { + let options = AttributedString.MarkdownParsingOptions( + interpretedSyntax: .inlineOnlyPreservingWhitespace) + // Only ~~double~~ tildes strike through, as in react-ui; a lone "~$5K" + // ("about") stays literal. + return (try? AttributedString(markdown: escapingLoneTildes(source), options: options)) + ?? AttributedString(source) + } + + private static func escapingLoneTildes(_ source: String) -> String { + let characters = Array(source) + var result = "" + for (index, character) in characters.enumerated() { + if character == "~", characters[safe: index - 1] != "~", characters[safe: index + 1] != "~" { + result += "\\~" + } else { + result.append(character) + } + } + return result + } +} + +/// Inline markdown with react-ui's citations: a run of `[1][2]` markers +/// becomes a small globe when the card has a source with a title and name for +/// one of them, and disappears otherwise. +struct CitedMarkdown: View { + let source: String + @Environment(\.openUICardSources) private var sources + + var body: some View { + let segments = Self.segments(source) + return segments.indices.reduce(Text("")) { text, index in + switch segments[index] { + case .text(let part): + return text + Text(InlineMarkdown.attributed(part)) + case .citation(let indices) where hasSource(indices): + // The marker pattern swallows the space after it; put it back before a word. + let spaced: Bool = { + guard index + 1 < segments.count, case .text(let next) = segments[index + 1] else { + return false + } + return next.first.map { $0.isLetter || $0.isNumber } ?? false + }() + return text + Text(Image(systemName: "globe")).font(.caption).foregroundStyle(.secondary) + + Text(spaced ? " " : "") + case .citation: + return text + } + } + } + + enum Segment: Equatable { + case text(String) + case citation([Int]) + } + + /// Splits on react-ui's citation pattern, `(\[\d+\]\s*)+`. + nonisolated static func segments(_ source: String) -> [Segment] { + let pattern = /(\[\d+\]\s*)+/ + var segments: [Segment] = [] + var rest = source[...] + while let match = rest.firstMatch(of: pattern) { + if match.range.lowerBound > rest.startIndex { + segments.append(.text(String(rest[.. Bool { + indices.contains { index in + guard index >= 1, index <= sources.count else { return false } + let source = sources[index - 1] + let title = source["title"].stringValue?.trimmingCharacters(in: .whitespaces) ?? "" + let name = source["sourceName"].stringValue?.trimmingCharacters(in: .whitespaces) ?? "" + return !title.isEmpty && !name.isEmpty + } + } +} + +private struct CardSourcesKey: EnvironmentKey { + static let defaultValue: [OpenUIValue] = [] +} + +extension EnvironmentValues { + /// The enclosing Card's `sources`, which inline citations refer to. + var openUICardSources: [OpenUIValue] { + get { self[CardSourcesKey.self] } + set { self[CardSourcesKey.self] = newValue } + } +} + +/// Block-level markdown: headings, bullet and numbered lists, quotes, code +/// fences, rules and paragraphs. Inline formatting uses `InlineMarkdown`. +struct MarkdownBlocks: View { + enum Block: Equatable { + case heading(level: Int, text: String) + case bullet(String) + case numbered(String, String) + case quote(String) + case code(language: String?, String) + /// A line that is only `![alt](url)`. + case image(alt: String, url: String) + /// `$$ … $$` math. There's no native TeX renderer, so it shows as source. + case math(String) + case table(header: [String], rows: [[String]]) + case rule + case paragraph(String) + } + + let blocks: [Block] + /// Renders `[n]` markers as citations (TextContent does, MarkDownRenderer doesn't). + let citations: Bool + @Environment(\.colorScheme) private var colorScheme + + init(_ source: String, citations: Bool = false) { + blocks = Self.parse(source) + self.citations = citations + } + + @ViewBuilder + private func inline(_ text: String) -> some View { + if citations { CitedMarkdown(source: text) } else { InlineMarkdown(text) } + } + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + ForEach(Array(blocks.enumerated()), id: \.offset) { _, block in + view(for: block) + } + } + } + + @ViewBuilder + private func view(for block: Block) -> some View { + switch block { + case .heading(let level, let text): + inline(text).font( + level <= 1 ? .title2.bold() : level == 2 ? .title3.bold() : .headline) + case .bullet(let text): + HStack(alignment: .firstTextBaseline, spacing: 6) { + Text("•") + inline(text) + } + case .numbered(let marker, let text): + HStack(alignment: .firstTextBaseline, spacing: 6) { + Text(marker).monospacedDigit() + inline(text) + } + case .quote(let text): + inline(text) + .foregroundStyle(.secondary) + .padding(.leading, 10) + .overlay(alignment: .leading) { Rectangle().fill(.secondary.opacity(0.4)).frame(width: 3) } + case .code(let language, let text): + // react-ui's markdown uses the CodeBlock, with oneLight in light mode. + CodeBlockContent( + code: text, language: language, theme: colorScheme == .dark ? .darkPlus : .oneLight) + case .image(let alt, let url): + RemoteImage(src: url, alt: alt, fit: true) + case .math(let tex): + Text(tex) + .font(.system(.callout, design: .monospaced)) + .padding(8) + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color.primary.opacity(0.04), in: RoundedRectangle(cornerRadius: 8)) + case .table(let header, let rows): + ScrollView(.horizontal, showsIndicators: false) { + Grid(alignment: .leading, horizontalSpacing: 16, verticalSpacing: 6) { + GridRow { + ForEach(header.indices, id: \.self) { inline(header[$0]).fontWeight(.semibold) } + } + Divider() + ForEach(rows.indices, id: \.self) { row in + GridRow { + ForEach(rows[row].indices, id: \.self) { inline(rows[row][$0]) } + } + } + } + } + case .rule: + Divider() + case .paragraph(let text): + inline(text) + } + } + + static func parse(_ source: String) -> [Block] { + var blocks: [Block] = [] + var paragraph: [String] = [] + var code: [String]? = nil + var codeLanguage: String? + var math: [String]? = nil + var table: [[String]] = [] + + func cells(_ line: String) -> [String] { + var trimmed = Substring(line) + if trimmed.hasPrefix("|") { trimmed = trimmed.dropFirst() } + if trimmed.hasSuffix("|") { trimmed = trimmed.dropLast() } + return trimmed.split(separator: "|", omittingEmptySubsequences: false).map { + $0.trimmingCharacters(in: .whitespaces) + } + } + + func flushTable() { + // A GFM table: header, a ---|--- separator, then rows. + if table.count >= 2, + table[1].allSatisfy({ !$0.isEmpty && $0.allSatisfy { "-: ".contains($0) } }) + { + blocks.append(.table(header: table[0], rows: Array(table.dropFirst(2)))) + } else { + paragraph.append(contentsOf: table.map { "| " + $0.joined(separator: " | ") + " |" }) + } + table = [] + } + + func flushParagraph() { + if !paragraph.isEmpty { + blocks.append(.paragraph(paragraph.joined(separator: "\n"))) + paragraph = [] + } + } + + for rawLine in source.components(separatedBy: "\n") { + let line = rawLine.trimmingCharacters(in: .whitespaces) + if var lines = math { + if line.hasSuffix("$$") { + lines.append(String(line.dropLast(2))) + let tex = lines.joined(separator: "\n").trimmingCharacters(in: .whitespacesAndNewlines) + blocks.append(.math(tex)) + math = nil + } else { + lines.append(rawLine) + math = lines + } + continue + } + if !table.isEmpty, !line.hasPrefix("|") { flushTable() } + if line.hasPrefix("$$"), code == nil { + flushParagraph() + let rest = line.dropFirst(2) + if rest.hasSuffix("$$"), rest.count >= 2 { + blocks.append(.math(String(rest.dropLast(2)).trimmingCharacters(in: .whitespaces))) + } else { + math = [String(rest)] + } + continue + } + if line.hasPrefix("|"), code == nil { + if table.isEmpty { flushParagraph() } + table.append(cells(line)) + continue + } + if line.hasPrefix("```") { + if let lines = code { + blocks.append(.code(language: codeLanguage, codeBlockText(lines))) + code = nil + } else { + flushParagraph() + code = [] + let info = line.dropFirst(3).split(separator: " ").first.map(String.init) + codeLanguage = info?.isEmpty == false ? info : nil + } + continue + } + if code != nil { + code?.append(rawLine) + continue + } + if line.isEmpty { + flushParagraph() + } else if let level = headingLevel(line) { + flushParagraph() + blocks.append(.heading(level: level, text: String(line.dropFirst(level + 1)))) + } else if line == "---" || line == "***" { + flushParagraph() + blocks.append(.rule) + } else if let first = line.first, "-*+".contains(first), line.dropFirst().first == " " { + flushParagraph() + blocks.append(.bullet(String(line.dropFirst(2)))) + } else if let dot = line.firstIndex(of: "."), line[..") { + flushParagraph() + blocks.append(.quote(String(line.dropFirst()).trimmingCharacters(in: .whitespaces))) + } else { + paragraph.append(line) + } + } + if !table.isEmpty { flushTable() } + if let lines = math { blocks.append(.math(lines.joined(separator: "\n"))) } + if let lines = code { blocks.append(.code(language: codeLanguage, codeBlockText(lines))) } + flushParagraph() + return blocks + } + + /// The level of a `#`–`######` heading line. + private static func headingLevel(_ line: String) -> Int? { + let hashes = line.prefix { $0 == "#" }.count + return (1...6).contains(hashes) && line.dropFirst(hashes).first == " " ? hashes : nil + } + + /// A fenced block's text, trimmed like react-ui's (`String(children).trim()`). + private static func codeBlockText(_ lines: [String]) -> String { + lines.joined(separator: "\n").trimmingCharacters(in: .whitespacesAndNewlines) + } + + /// `![alt](url)` (optionally with a title) alone on a line. + static func image(_ line: String) -> Block? { + guard + let match = line.wholeMatch(of: /!\[([^\]]*)\]\((\S+?)(?:\s+"[^"]*")?\)/) + else { return nil } + return .image(alt: String(match.1), url: String(match.2)) + } +} + +/// Lays children out in rows, wrapping to the next row when one is full. A +/// child wider than a row gets the row's width, like CSS's `max-width: 100%`. +struct FlowLayout: Layout { + var spacing: CGFloat = 8 + /// `.center` centers each row, like react-ui's centered legends. + var alignment = HorizontalAlignment.leading + + func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { + let rows = arrange(subviews, width: proposal.width ?? .infinity) + let width = rows.map(\.width).max() ?? 0 + let height = rows.map(\.height).reduce(0, +) + spacing * CGFloat(max(rows.count - 1, 0)) + return CGSize(width: proposal.width ?? width, height: height) + } + + func placeSubviews( + in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout () + ) { + var y = bounds.minY + for row in arrange(subviews, width: bounds.width) { + var x = alignment == .center ? bounds.minX + (bounds.width - row.width) / 2 : bounds.minX + for (index, size) in row.items { + subviews[index].place(at: CGPoint(x: x, y: y), proposal: ProposedViewSize(size)) + x += size.width + spacing + } + y += row.height + spacing + } + } + + private struct Row { + var items: [(index: Int, size: CGSize)] = [] + var width: CGFloat = 0 + var height: CGFloat = 0 + } + + private func arrange(_ subviews: Subviews, width: CGFloat) -> [Row] { + var rows: [Row] = [] + var current = Row() + for index in subviews.indices { + var size = subviews[index].sizeThatFits(.unspecified) + if size.width > width { + size = subviews[index].sizeThatFits(ProposedViewSize(width: width, height: nil)) + } + let needed = current.items.isEmpty ? size.width : current.width + spacing + size.width + if needed > width, !current.items.isEmpty { + rows.append(current) + current = Row() + } + current.width = current.items.isEmpty ? size.width : current.width + spacing + size.width + current.height = max(current.height, size.height) + current.items.append((index, size)) + } + if !current.items.isEmpty { rows.append(current) } + return rows + } +} diff --git a/packages/swift-lang/Sources/OpenUISwiftUI/SyntaxHighlighter.swift b/packages/swift-lang/Sources/OpenUISwiftUI/SyntaxHighlighter.swift new file mode 100644 index 0000000000..39ec24c8b4 --- /dev/null +++ b/packages/swift-lang/Sources/OpenUISwiftUI/SyntaxHighlighter.swift @@ -0,0 +1,364 @@ +import SwiftUI + +/// Syntax colors for code, standing in for react-ui's Prism highlighting: the +/// same token kinds and the same two themes (`vscDarkPlus` for `CodeBlock`, +/// `oneLight` or `vscDarkPlus` for code in markdown). A small tokenizer +/// instead of a dependency: comments, strings, numbers, keywords, calls, types, +/// properties, JSON keys and markup tags cover what models write. +enum SyntaxHighlighter { + enum Token: Equatable { + case plain, comment, string, number, keyword, control, constant, function, type, property + case tag, attribute, punctuation + } + + /// Colors per token kind, from react-syntax-highlighter's Prism themes. + struct Theme: Sendable { + var background: Color + var plain: Color + var colors: [Token: Color] + /// Prism themes color a language's plain words differently, e.g. + /// `vscDarkPlus` shows JavaScript variables in light blue. + var languagePlain: [String: Color] = [:] + + func color(_ token: Token, language: String?) -> Color { + if token == .plain { return language.flatMap { languagePlain[$0.lowercased()] } ?? plain } + return colors[token] ?? plain + } + + static let darkPlus = Theme( + background: hex(0x1E1E1E), plain: hex(0xD4D4D4), + colors: [ + .comment: hex(0x6A9955), .string: hex(0xCE9178), .number: hex(0xB5CEA8), + .keyword: hex(0x569CD6), .control: hex(0xC586C0), .constant: hex(0x569CD6), + .function: hex(0xDCDCAA), .type: hex(0x4EC9B0), .property: hex(0x9CDCFE), + .tag: hex(0x569CD6), .attribute: hex(0x9CDCFE), .punctuation: hex(0xD4D4D4), + ], + languagePlain: [ + "javascript": hex(0x9CDCFE), "js": hex(0x9CDCFE), "jsx": hex(0x9CDCFE), + "typescript": hex(0x9CDCFE), "ts": hex(0x9CDCFE), "tsx": hex(0x9CDCFE), + "css": hex(0xCE9178), + ]) + + static let oneLight = Theme( + background: hsl(230, 1, 98), plain: hsl(230, 8, 24), + colors: [ + .comment: hsl(230, 4, 64), .string: hsl(119, 34, 47), .number: hsl(35, 99, 36), + .keyword: hsl(301, 63, 40), .control: hsl(301, 63, 40), .constant: hsl(35, 99, 36), + .function: hsl(221, 87, 60), .type: hsl(35, 99, 36), .property: hsl(5, 74, 59), + .tag: hsl(5, 74, 59), .attribute: hsl(35, 99, 36), .punctuation: hsl(230, 8, 24), + ]) + + private static func hex(_ value: Int) -> Color { + Color( + red: Double((value >> 16) & 0xFF) / 255, green: Double((value >> 8) & 0xFF) / 255, + blue: Double(value & 0xFF) / 255) + } + + private static func hsl(_ hue: Double, _ saturation: Double, _ lightness: Double) -> Color { + let s = saturation / 100 + let l = lightness / 100 + let brightness = l + s * min(l, 1 - l) + return Color( + hue: hue / 360, saturation: brightness == 0 ? 0 : 2 * (1 - l / brightness), + brightness: brightness) + } + } + + /// `code` as colored text. + static func highlight(_ code: String, language: String?, theme: Theme) -> AttributedString { + var result = AttributedString() + for (text, token) in tokenize(code, language: language) { + var run = AttributedString(text) + run.foregroundColor = theme.color(token, language: language) + result += run + } + return result + } + + // MARK: Tokenizing + + /// The code split into colored runs. Joining the runs gives back `code`. + static func tokenize(_ code: String, language: String?) -> [(String, Token)] { + let grammar = Grammar(language) + let chars = Array(code) + var runs: [(String, Token)] = [] + var plain = "" + var index = 0 + + func emit(_ end: Int, _ token: Token) { + if !plain.isEmpty { + runs.append((plain, .plain)) + plain = "" + } + runs.append((String(chars[index.. Bool { + let prefix = Array(prefix) + return position + prefix.count <= chars.count + && Array(chars[position.. Int { chars[from...].firstIndex(of: "\n") ?? chars.count } + + while index < chars.count { + let char = chars[index] + if grammar.markup { + if char == "<" { + if starts("", from: index + 4, in: chars) + emit(end, .comment) + continue + } + // ``. + var end = index + 1 + if end < chars.count, chars[end] == "/" { end += 1 } + while end < chars.count, + chars[end].isLetter || chars[end].isNumber || "-:".contains(chars[end]) + { + end += 1 + } + if end > index + 1 { + emit(end, .tag) + continue + } + } + if char == "=" || char == ">" || char == "/" { + emit(index + 1, .punctuation) + continue + } + if char == "\"" || char == "'" { + emit(stringEnd(chars, from: index, quote: char), .string) + continue + } + if char.isLetter, isInsideTag(chars, index) { + var end = index + while end < chars.count, + chars[end].isLetter || chars[end].isNumber || "-:".contains(chars[end]) + { + end += 1 + } + emit(end, .attribute) + continue + } + plain.append(char) + index += 1 + continue + } + + // Comments. + if grammar.lineComments.contains(where: { starts($0, at: index) }) { + emit(lineEnd(index), .comment) + continue + } + if grammar.blockComments, starts("/*", at: index) { + emit(closing("*/", from: index + 2, in: chars), .comment) + continue + } + // Strings, including triple-quoted ones. + if grammar.quotes.contains(char) { + let triple = String(repeating: char, count: 3) + if grammar.tripleQuotes, starts(triple, at: index) { + emit(closing(triple, from: index + 3, in: chars), .string) + } else { + let end = stringEnd(chars, from: index, quote: char) + // A JSON key is a string followed by a colon. + var next = end + while next < chars.count, chars[next] == " " { next += 1 } + let isKey = grammar.json && next < chars.count && chars[next] == ":" + emit(end, isKey ? .property : .string) + } + continue + } + // Numbers. + if char.isNumber || (char == "." && index + 1 < chars.count && chars[index + 1].isNumber), + index == 0 || !isIdentifierCharacter(chars[index - 1]) + { + var end = index + 1 + while end < chars.count, + chars[end].isHexDigit || "xXoObB_.".contains(chars[end]) + || ((chars[end] == "+" || chars[end] == "-") && "eE".contains(chars[end - 1])) + { + end += 1 + } + emit(end, .number) + continue + } + // Words: keywords, constants, calls, types and properties. + if isIdentifierStart(char) { + var end = index + 1 + while end < chars.count, isIdentifierCharacter(chars[end]) { end += 1 } + let word = String(chars[index.. 0, chars[index - 1] == "." { + token = .property + } else if word.first?.isUppercase == true, grammar.typesAreCapitalized { + token = .type + } else { + token = .plain + } + emit(end, token) + continue + } + // Operators and punctuation have their own color; whitespace has none. + if char.isWhitespace { + plain.append(char) + index += 1 + } else { + emit(index + 1, .punctuation) + } + } + if !plain.isEmpty { runs.append((plain, .plain)) } + return runs + } + + private static func isIdentifierStart(_ char: Character) -> Bool { + char.isLetter || char == "_" || char == "$" + } + + private static func isIdentifierCharacter(_ char: Character) -> Bool { + char.isLetter || char.isNumber || char == "_" || char == "$" + } + + /// The end of a quoted string, after its closing quote (or the line's end + /// for an unfinished one, as happens mid-stream). + private static func stringEnd(_ chars: [Character], from start: Int, quote: Character) -> Int { + var index = start + 1 + while index < chars.count { + if chars[index] == "\\" { + index += 2 + continue + } + if chars[index] == quote { return index + 1 } + if chars[index] == "\n", quote != "`" { return index } + index += 1 + } + return chars.count + } + + /// The end of a block (comment or triple-quoted string) closed by `marker`. + private static func closing(_ marker: String, from start: Int, in chars: [Character]) -> Int { + let marker = Array(marker) + var index = start + while index + marker.count <= chars.count { + if Array(chars[index..`. + private static func isInsideTag(_ chars: [Character], _ index: Int) -> Bool { + var position = index - 1 + while position >= 0 { + if chars[position] == ">" { return false } + if chars[position] == "<" { return true } + position -= 1 + } + return false + } + + /// What a language's code looks like, as far as coloring goes. + struct Grammar { + var keywords: Set = [] + var control: Set = [] + var constants: Set = [ + "true", "false", "null", "nil", "None", "True", "False", "undefined", + ] + var lineComments = ["//"] + var blockComments = true + var quotes: Set = ["\"", "'", "`"] + var tripleQuotes = false + var typesAreCapitalized = true + var json = false + var markup = false + + init(_ language: String?) { + let control: Set = [ + "if", "else", "for", "while", "do", "switch", "case", "default", "break", "continue", + "return", "try", "catch", "finally", "throw", "throws", "guard", "import", "export", + "from", "await", "yield", "in", "of", "elif", "except", "raise", "with", "pass", "match", + "loop", "defer", "go", "select", "goto", "then", "fi", "done", "esac", "unless", "until", + "begin", "rescue", "ensure", "end", + ] + self.control = control + switch (language ?? "").lowercased() { + case "json", "jsonc": + keywords = [] + self.control = [] + lineComments = [] + blockComments = false + quotes = ["\""] + json = true + typesAreCapitalized = false + case "html", "xml", "svg", "vue": + markup = true + case "python", "py": + keywords = [ + "def", "class", "lambda", "global", "nonlocal", "assert", "del", "async", "as", "is", + "not", "and", "or", "print", + ] + lineComments = ["#"] + blockComments = false + quotes = ["\"", "'"] + tripleQuotes = true + case "ruby", "rb": + keywords = [ + "def", "class", "module", "self", "do", "and", "or", "not", "attr_accessor", "require", + ] + lineComments = ["#"] + blockComments = false + quotes = ["\"", "'"] + case "bash", "sh", "shell", "zsh", "console": + keywords = ["export", "local", "function", "echo", "cd", "set", "unset", "source"] + lineComments = ["#"] + blockComments = false + quotes = ["\"", "'"] + typesAreCapitalized = false + case "yaml", "yml", "toml", "ini": + keywords = [] + self.control = [] + lineComments = ["#"] + blockComments = false + quotes = ["\"", "'"] + typesAreCapitalized = false + case "sql": + let words = [ + "select", "from", "where", "join", "left", "right", "inner", "outer", "on", "group", "by", + "order", "having", "limit", "insert", "into", "values", "update", "set", "delete", + "create", "table", "index", "as", "and", "or", "not", "null", "distinct", "union", + "primary", "key", "references", "alter", "drop", "count", "sum", "avg", "min", "max", + ] + keywords = Set(words + words.map { $0.uppercased() }) + self.control = [] + lineComments = ["--"] + quotes = ["'", "\""] + typesAreCapitalized = false + default: + // C-family and most others: JS/TS, Swift, Kotlin, Java, Go, Rust, C, C++, C#, PHP. + keywords = [ + "var", "let", "const", "function", "func", "fn", "def", "class", "struct", "enum", + "interface", "protocol", "extension", "type", "typealias", "impl", "trait", "mod", + "pub", "private", "public", "protected", "internal", "fileprivate", "static", "final", + "override", "abstract", "async", "new", "this", "self", "super", "extends", + "implements", "package", "namespace", "using", "val", "fun", "object", "data", "sealed", + "open", "lateinit", "mut", "ref", "where", "as", "is", "instanceof", "typeof", "void", + "int", "float", "double", "char", "bool", "boolean", "string", "long", "short", "byte", + "unsigned", "signed", "const_cast", "auto", "inout", "some", "any", "readonly", "declare", + "keyof", "get", "set", "init", "deinit", "subscript", "mutating", "weak", "unowned", + "lazy", + "chan", "map", "range", "echo", + ] + } + } + } +} diff --git a/packages/swift-lang/Sources/OpenUISwiftUI/Theme.swift b/packages/swift-lang/Sources/OpenUISwiftUI/Theme.swift new file mode 100644 index 0000000000..71b6ea1371 --- /dev/null +++ b/packages/swift-lang/Sources/OpenUISwiftUI/Theme.swift @@ -0,0 +1,119 @@ +import SwiftUI + +/// Spacing, shapes and colors the built-in components use. Override parts of +/// it with `.environment(\.openUITheme, theme)`. +/// +/// The defaults follow react-ui's design tokens (spacing, radii, and the +/// translucent fills and borders), so both renderers look alike. +public struct OpenUITheme: Sendable { + public var spacing: CGFloat = 12 + public var compactSpacing: CGFloat = 6 + public var cornerRadius: CGFloat = 10 + public var smallCornerRadius: CGFloat = 6 + public var cardPadding: CGFloat = 18 + /// Primary buttons, selected options, steps and other emphasis. The + /// renderer also tints its controls with it. + public var accent: Color = .accentColor + /// Text and icons on accent backgrounds, like primary buttons (react-ui's + /// `textAccentPrimary`). Use a dark color with a light accent. + public var onAccent: Color = .white + /// Raised surfaces such as clickable cards (react-ui's `foreground`). + public var surface: Color = .platformElevatedBackground + /// Recessed fills such as inputs (react-ui's `sunk`). + public var sunkSurface: Color = .primary.opacity(0.04) + /// The faint fill of static cards (react-ui's `sunk-light`). + public var subtleSurface: Color = .primary.opacity(0.02) + public var border: Color = .primary.opacity(0.06) + /// Borders of clickable cards (react-ui's `border-interactive`). + public var interactiveBorder: Color = .primary.opacity(0.12) + public var chartHeight: CGFloat = 220 + /// Colors for chart series, like react-ui's `defaultChartPalette`: a ramp + /// that charts pick from the middle outwards. `nil` uses react-ui's default + /// blue ramp. + public var chartPalette: [Color]? + /// A palette for one kind of chart, like react-ui's `barChartPalette` and + /// the rest. A chart uses its own, then `chartPalette`, then the default. + public var barChartPalette: [Color]? + public var horizontalBarChartPalette: [Color]? + public var lineChartPalette: [Color]? + public var areaChartPalette: [Color]? + public var pieChartPalette: [Color]? + public var radialChartPalette: [Color]? + public var radarChartPalette: [Color]? + /// Status colors (react-ui's info, success, alert and danger text): callouts, + /// tags, trends, form errors and confirmations. + public var info: Color = .blue + public var success: Color = .green + public var alert: Color = .orange + public var danger: Color = .red + + public init() {} + + public static let `default` = OpenUITheme() +} + +private struct ThemeKey: EnvironmentKey { + static let defaultValue = OpenUITheme.default +} + +extension EnvironmentValues { + public var openUITheme: OpenUITheme { + get { self[ThemeKey.self] } + set { self[ThemeKey.self] = newValue } + } +} + +extension Color { + /// White in light mode and a raised gray in dark mode. + static var platformElevatedBackground: Color { + #if os(macOS) + Color(nsColor: .controlBackgroundColor) + #else + Color(uiColor: .secondarySystemGroupedBackground) + #endif + } +} + +extension OpenUITheme { + /// The color for a status variant of a callout, tag or icon. + func status(_ variant: String?) -> Color { + switch variant { + case "info": info + case "warning": alert + case "error", "danger": danger + case "success": success + default: .secondary + } + } + + /// The colors a chart picks from: its own palette, then `chartPalette` + /// (nil leaves react-ui's default ramp), like react-ui's `resolvePalette`. + func chartRamp(_ own: [Color]?) -> [Color]? { + [own, chartPalette].first { $0?.isEmpty == false } ?? nil + } +} + +/// The surface behind a "card" / "sunk" / "clear" container variant. +struct SurfaceModifier: ViewModifier { + let variant: String + @Environment(\.openUITheme) private var theme + + func body(content: Content) -> some View { + switch variant { + case "clear": + content + case "sunk": + content.padding(theme.cardPadding) + .background(theme.sunkSurface, in: RoundedRectangle(cornerRadius: 16)) + .overlay(RoundedRectangle(cornerRadius: 16).strokeBorder(theme.border)) + default: + // react-ui's "card" variant is a plain full-width column; the chat + // bubble around it provides the frame. + content + } + } +} + +extension View { + func surface(_ variant: String) -> some View { modifier(SurfaceModifier(variant: variant)) } +} diff --git a/packages/swift-lang/Tests/OpenUILangTests/EditAndCloudTests.swift b/packages/swift-lang/Tests/OpenUILangTests/EditAndCloudTests.swift new file mode 100644 index 0000000000..9bc75e86fc --- /dev/null +++ b/packages/swift-lang/Tests/OpenUILangTests/EditAndCloudTests.swift @@ -0,0 +1,158 @@ +import CryptoKit +import Foundation +import Testing + +@testable import OpenUILang + +/// `mergeStatements` must match lang-core's edit-mode merge. +@Suite struct MergeConformanceTests { + @Test(arguments: FixtureCase.all("merge")) + func merge(_ fixture: FixtureCase) { + let value = fixture.value + let merged = mergeStatements( + value["existing"].stringValue!, value["patch"].stringValue!, + rootId: value["rootId"].stringValue ?? "root") + let expected = value["expected"].stringValue! + #expect(merged == expected, "\(fixture.name): \(firstLineDifference(merged, expected) ?? "")") + } +} + +/// `generateCloudConfig` must produce the same config block, or the same +/// error, as `generateSystemPrompt({ cloud: true })`. +@Suite struct CloudConfigConformanceTests { + static let fixtures = Fixtures.load("cloud") + + static var cases: [FixtureCase] { + (fixtures["cases"].arrayValue ?? []).map { + FixtureCase(name: $0["name"].stringValue ?? "?", value: $0) + } + } + + @Test(arguments: cases) + func config(_ fixture: FixtureCase) { + let spec = fixture.value["spec"] + let result = Result { + try generateCloudConfig( + library: spec["library"], promptOptions: Self.promptOptions(spec["promptOptions"]), + instructions: spec["instructions"].stringValue) + } + switch result { + case .success(let config): + #expect(config == fixture.value["expected"].stringValue, "\(fixture.name)") + case .failure(let error): + #expect("\(error)" == fixture.value["error"].stringValue, "\(fixture.name)") + } + } + + @Test func chatLibraryConfigMatchesLangCore() throws { + // lang-core's config for openuiChatLibrary with its prompt options is + // ~80 KB, so the fixture keeps its length and SHA-256. + let library = Library( + components: ChatComponents.all.map { ComponentDefinition($0, content: ()) }, root: "Card", + componentGroups: ChatComponents.groups) + let config = try library.cloudConfig(ChatComponents.promptOptions) + + let expected = Self.fixtures["chat"] + #expect(Double(config.utf16.count) == expected["length"].numberValue) + let digest = SHA256.hash(data: Data(config.utf8)).map { String(format: "%02x", $0) }.joined() + #expect(digest == expected["sha256"].stringValue) + } + + static func promptOptions(_ value: OpenUIValue) -> PromptOptions? { + guard value.objectValue != nil else { return nil } + let strings = { (key: String) in value[key].arrayValue?.compactMap(\.stringValue) } + return PromptOptions( + preamble: value["preamble"].stringValue, additionalRules: strings("additionalRules"), + examples: strings("examples"), tools: strings("tools")?.map(ToolDescriptor.name), + editMode: value["editMode"].boolValue) + } +} + +/// The Cloud message format must split and wrap messages exactly like +/// react-ui's sentinel parser. +@Suite struct CloudMessageConformanceTests { + static let fixtures = Fixtures.load("messages") + + static var cases: [FixtureCase] { + (fixtures["separate"].arrayValue ?? []).map { + FixtureCase(name: $0["name"].stringValue ?? "?", value: $0) + } + } + + @Test(arguments: cases) + func separatesContentAndContext(_ fixture: FixtureCase) { + let parsed = separateContentAndContext(fixture.value["input"].stringValue!) + let expected = fixture.value["expected"] + #expect(parsed.content == expected["content"].stringValue) + #expect(parsed.contextString == expected["contextString"].stringValue) + #expect(parsed.contentHeader == expected["contentHeader"].stringValue) + #expect(parsed.end == (expected["end"].boolValue ?? false)) + } + + @Test func detectsLangSyntax() { + for entry in Self.fixtures["langSyntax"].arrayValue ?? [] { + let input = entry["input"].stringValue! + #expect(hasLangSyntax(input) == entry["expected"].boolValue, "\(input.debugDescription)") + } + #expect(!hasLangSyntax(nil)) + } + + @Test func parsesArtifacts() { + for entry in Self.fixtures["artifacts"].arrayValue ?? [] { + let input = entry["input"].stringValue! + let parsed = parseArtifactSentinel(input) + let expected = entry["expected"] + guard expected != .null else { + #expect(parsed == nil, "\(input.debugDescription)") + continue + } + let header = expected["header"] + #expect(parsed?.header.artifactId == header["artifact_id"].stringValue) + #expect(parsed?.header.type.rawValue == header["type"].stringValue) + #expect(parsed?.header.name == header["name"].stringValue) + #expect(parsed?.header.version == header["version"].stringValue) + #expect(parsed?.program == expected["program"].stringValue) + } + } + + @Test func wraps() { + let wrap = Self.fixtures["wrap"] + #expect(wrapContent("root = X()") == wrap["content"].stringValue) + #expect( + wrapContentWithHeader("root = X()", "]]>openui:content?thesys=true") + == wrap["contentWithHeader"].stringValue) + #expect(wrapContentWithHeader("root = X()", nil) == wrap["contentWithoutHeader"].stringValue) + #expect(wrapContext(#"[{"a":1}]"#) == wrap["context"].stringValue) + } + + /// The two messages react-ui's chat builds (GenUIAssistantMessage). + @Test func buildsChatMessages() throws { + var event = ActionEvent( + type: BuiltinActionType.continueConversation, humanFriendlyMessage: "Book it") + #expect( + continueConversationMessage(event) + == "]]>openui:content\nBook it\n]]>openui:context\n[\"User clicked: Book it\"]") + event.formState = ["form": ["seats": ["value": 2, "componentType": "Input"]]] + #expect( + continueConversationMessage(event) + == #"]]>openui:content\#nBook it\#n]]>openui:context\#n["User clicked: Book it",{"form":{"seats":{"value":2,"componentType":"Input"}}}]"# + ) + + let state: OpenUIObject = ["$tab": "b"] + let saved = messageWithState( + content: "root = X()", contentHeader: "]]>openui:content?thesys=true", state: state) + #expect( + saved == "]]>openui:content?thesys=true\nroot = X()\n]]>openui:context\n[{\"$tab\":\"b\"}]") + #expect( + messageWithState(content: "root = X()", contentHeader: nil, state: [:]) + == wrapContent("root = X()")) + + // Round trip: what was saved comes back as the renderer's initial state. + let parsed = separateContentAndContext(saved) + #expect(parsed.content == "root = X()") + #expect(initialState(fromContext: parsed.contextString) == state) + #expect(initialState(fromContext: #"{"a":1}"#) == ["a": 1]) + #expect(initialState(fromContext: "[null]") == nil) + #expect(initialState(fromContext: "not json") == nil) + } +} diff --git a/packages/swift-lang/Tests/OpenUILangTests/ErrorReportingTests.swift b/packages/swift-lang/Tests/OpenUILangTests/ErrorReportingTests.swift new file mode 100644 index 0000000000..710e957cf9 --- /dev/null +++ b/packages/swift-lang/Tests/OpenUILangTests/ErrorReportingTests.swift @@ -0,0 +1,87 @@ +import Testing + +@testable import OpenUILang + +/// `OpenUIRuntime.onError` must report what react-lang's `onError` does. +@MainActor +@Suite struct ErrorReportingTests { + static let library = Library( + components: ChatComponents.all.map { ComponentDefinition($0, content: ()) }, root: "Card", + componentGroups: ChatComponents.groups) + + /// Collects every `onError` call. + final class Reports { + var calls: [[OpenUIError]] = [] + } + + func makeRuntime(toolProvider: (any ToolProvider)? = nil) -> (OpenUIRuntime, Reports) { + let runtime = OpenUIRuntime(library: Self.library, toolProvider: toolProvider) + let reports = Reports() + runtime.onError = { reports.calls.append($0) } + return (runtime, reports) + } + + @Test(arguments: FixtureCase.all("errors")) + func parserErrorHintsMatchLangCore(_ fixture: FixtureCase) { + let (runtime, reports) = makeRuntime() + runtime.update(response: fixture.value["input"].stringValue!, isStreaming: false) + let reported = OpenUIValue.array((reports.calls.last ?? []).map(\.jsonRepresentation)) + let diff = firstDifference(normalized(reported), normalized(fixture.value["expected"])) + #expect(diff == nil, "\(fixture.name): \(diff ?? "")") + } + + @Test func reportsOnlyAfterStreamingAndOnlyOnChange() { + let (runtime, reports) = makeRuntime() + runtime.update(response: "root = Card([Mystery(", isStreaming: true) + #expect(reports.calls.isEmpty) + + runtime.update(response: "root = Card([Mystery()])", isStreaming: false) + #expect(reports.calls.map { $0.map(\.code) } == [["unknown-component"]]) + runtime.update(response: "root = Card([Mystery()])", isStreaming: false) + #expect(reports.calls.count == 1) + + // The next response clears the errors as soon as it starts streaming. + runtime.update(response: "root = Card([", isStreaming: true) + runtime.update(response: "root = Card([TextContent(", isStreaming: true) + #expect(reports.calls.count == 2 && reports.calls[1].isEmpty) + + runtime.update(response: "root = Card([TextContent(\"ok\")])", isStreaming: false) + #expect(reports.calls.count == 3 && reports.calls[2].isEmpty) + } + + @Test func reportsAResponseWithoutARoot() { + let (runtime, reports) = makeRuntime() + runtime.update(response: "Here is a summary in plain text.", isStreaming: false) + let error = reports.calls.last?.first + #expect(error?.code == "parse-failed") + #expect(error?.message == "Code parsed but produced no renderable root component") + #expect( + error?.hint + == "The entire response must be valid openui-lang code starting with root = Card(...)") + } + + @Test func reportsFailedQueriesWhenTheyFinish() async { + let (runtime, reports) = makeRuntime( + toolProvider: FunctionToolProvider(["other": { _ in .null }])) + runtime.update( + response: "root = Card([TextContent(\"x\")])\nq = Query(\"missing_tool\", {}, {})", + isStreaming: false) + for _ in 0..<20 { await Task.yield() } + let last = reports.calls.last ?? [] + #expect(last.map(\.code) == ["tool-not-found"]) + #expect(last.first?.toolName == "missing_tool") + } + + @Test func reportsParseResultChanges() { + let runtime = OpenUIRuntime(library: Self.library) + var results: [ParseResult?] = [] + runtime.onParseResult = { results.append($0) } + runtime.update(response: nil, isStreaming: false) + runtime.update(response: "root = Card([", isStreaming: true) + runtime.update(response: "root = Card([", isStreaming: false) + runtime.update(response: "root = Card([TextContent(\"a\")])", isStreaming: false) + #expect(results.count == 3) + #expect(results[0] == nil) + #expect(results[2]?.root?.typeName == "Card") + } +} diff --git a/packages/swift-lang/Tests/OpenUILangTests/EvaluationConformanceTests.swift b/packages/swift-lang/Tests/OpenUILangTests/EvaluationConformanceTests.swift new file mode 100644 index 0000000000..031df391ca --- /dev/null +++ b/packages/swift-lang/Tests/OpenUILangTests/EvaluationConformanceTests.swift @@ -0,0 +1,41 @@ +import Testing + +@testable import OpenUILang + +extension Fixtures { + static func schemaContext(_ schemaName: String) -> SchemaContext { + let reactive = schemas[schemaName]["reactive"].objectValue ?? OpenUIObject() + return SchemaContext { component, prop in + (reactive[component]?.arrayValue ?? []).contains(.string(prop)) + } + } +} + +/// Prop evaluation must match lang-core's `evaluateElementProps`. +@Suite struct EvaluationConformanceTests { + @Test(arguments: FixtureCase.all("evaluation")) + func evaluateTree(_ fixture: FixtureCase) { + let schema = fixture.value["schema"].stringValue! + let result = OpenUILang.parse( + fixture.value["input"].stringValue!, Fixtures.paramMap(schema), + rootName: Fixtures.rootName(schema)) + + let store = Store() + store.initialize(defaults: result.stateDeclarations, persisted: OpenUIObject()) + for (key, value) in fixture.value["state"].objectValue ?? OpenUIObject() { + store.set(key, value) + } + let queryResults = fixture.value["queryResults"].objectValue ?? OpenUIObject() + let context = EvaluationContext( + getState: { unwrapFieldValue(store.get($0)) }, + resolveRef: { queryResults[$0] ?? .null }) + + let root = result.root.map { + evaluateElementProps($0, context, Fixtures.schemaContext(schema)) + } + let diff = firstDifference( + normalized(root.map(\.jsonRepresentation) ?? .null), + normalized(fixture.value["expected"]["root"])) + #expect(diff == nil, "\(fixture.name): \(diff ?? "")") + } +} diff --git a/packages/swift-lang/Tests/OpenUILangTests/Fixtures.swift b/packages/swift-lang/Tests/OpenUILangTests/Fixtures.swift new file mode 100644 index 0000000000..c95291918b --- /dev/null +++ b/packages/swift-lang/Tests/OpenUILangTests/Fixtures.swift @@ -0,0 +1,67 @@ +import Foundation +import Testing + +@testable import OpenUILang + +/// Loads the conformance fixtures generated from @openuidev/lang-core +/// (see Scripts/generate-fixtures.mjs). +enum Fixtures { + static func load(_ name: String) -> OpenUIValue { + let url = Bundle.module.url(forResource: name, withExtension: "json", subdirectory: "Fixtures")! + let text = try! String(contentsOf: url, encoding: .utf8) + return try! JSON.parse(text) + } + + static let schemas = load("schemas") + + static func paramMap(_ schemaName: String) -> ParamMap { + compileSchema(schemas[schemaName]["schema"]) + } + + static func rootName(_ schemaName: String) -> String? { + schemas[schemaName]["root"].stringValue + } +} + +/// A named fixture case, printable in test output. +struct FixtureCase: CustomTestStringConvertible, Sendable { + let name: String + let value: OpenUIValue + var testDescription: String { name } + + static func all(_ file: String) -> [FixtureCase] { + (Fixtures.load(file).arrayValue ?? []).map { + FixtureCase(name: $0["name"].stringValue ?? "?", value: $0) + } + } +} + +/// Round-trips through JSON text so both sides get identical treatment of +/// `undefined`, NaN and -0, exactly as `JSON.stringify` would. +func normalized(_ value: OpenUIValue) -> OpenUIValue { + try! JSON.parse(JSON.stringify(value)) +} + +/// The path of the first difference between two JSON values, or nil if they +/// are equivalent. Object key order is ignored; array order is not. +func firstDifference(_ actual: OpenUIValue, _ expected: OpenUIValue, path: String = "$") -> String? +{ + switch (actual, expected) { + case (.object(let a), .object(let e)): + for key in Set(a.keys).union(e.keys).sorted() { + guard let av = a[key] else { return "\(path).\(key): missing in Swift result" } + guard let ev = e[key] else { return "\(path).\(key): unexpected in Swift result" } + if let diff = firstDifference(av, ev, path: "\(path).\(key)") { return diff } + } + return nil + case (.array(let a), .array(let e)): + if a.count != e.count { return "\(path): \(a.count) items, expected \(e.count)" } + for index in a.indices { + if let diff = firstDifference(a[index], e[index], path: "\(path)[\(index)]") { return diff } + } + return nil + default: + return actual == expected + ? nil : "\(path): got \(JSON.stringify(actual)), expected \(JSON.stringify(expected))" + } +} diff --git a/packages/swift-lang/Tests/OpenUILangTests/Fixtures/chat-spec.json b/packages/swift-lang/Tests/OpenUILangTests/Fixtures/chat-spec.json new file mode 100644 index 0000000000..4a32f0c3a3 --- /dev/null +++ b/packages/swift-lang/Tests/OpenUILangTests/Fixtures/chat-spec.json @@ -0,0 +1,5 @@ +{ +"root": "Card", +"components": {"Card":{"signature":"Card(children: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock | ListBlock | FollowUpBlock | SectionBlock | Tabs | Carousel)[], sources?: {url?: string, title: string, sourceName: string}[])","description":"Vertical container for all content in a chat response. Children stack top to bottom automatically. Optional sources ([{ title, sourceName, url }]) render as a Sources strip at the bottom and back inline [n] citations in TextContent."},"CardHeader":{"signature":"CardHeader(title?: string, subtitle?: string)","description":"Header with optional title and subtitle"},"TextContent":{"signature":"TextContent(text: string, size?: \"small\" | \"default\" | \"large\" | \"small-heavy\" | \"large-heavy\")","description":"Text block. Supports markdown. Optional size: \"small\" | \"default\" | \"large\" | \"small-heavy\" | \"large-heavy\"."},"MarkDownRenderer":{"signature":"MarkDownRenderer(textMarkdown: string, variant?: \"clear\" | \"card\" | \"sunk\")","description":"Renders markdown text with optional container variant"},"Callout":{"signature":"Callout(variant: \"info\" | \"warning\" | \"error\" | \"success\" | \"neutral\", title: string, description: string, visible?: $binding)","description":"Callout banner. Optional visible is a reactive $boolean — auto-dismisses after 3s by setting $visible to false."},"TextCallout":{"signature":"TextCallout(variant?: \"neutral\" | \"info\" | \"warning\" | \"success\" | \"danger\", title?: string, description?: string)","description":"Text callout with variant, title, and description"},"Image":{"signature":"Image(alt: string, src?: string)","description":"Image with alt text and optional URL"},"ImageBlock":{"signature":"ImageBlock(src: string, alt?: string)","description":"Image block with loading state"},"ImageGallery":{"signature":"ImageGallery(images: {src: string, alt?: string, details?: string}[])","description":"Gallery grid of images with modal preview"},"CodeBlock":{"signature":"CodeBlock(language: string, codeString: string)","description":"Syntax-highlighted code block"},"Separator":{"signature":"Separator(orientation?: \"horizontal\" | \"vertical\", decorative?: boolean)","description":"Visual divider between content sections"},"Table":{"signature":"Table(columns: Col[])","description":"Data table — column-oriented. Each Col holds its own data array."},"Col":{"signature":"Col(label: string, data: any, type?: \"string\" | \"number\" | \"action\")","description":"Column definition — holds label + data array"},"BarChart":{"signature":"BarChart(labels: string[], series: Series[], variant?: \"grouped\" | \"stacked\", xLabel?: string, yLabel?: string, height?: number)","description":"Vertical bars; use for comparing values across categories with one or more series"},"LineChart":{"signature":"LineChart(labels: string[], series: Series[], variant?: \"linear\" | \"natural\" | \"step\", xLabel?: string, yLabel?: string, height?: number)","description":"Lines over categories; use for trends and continuous data over time"},"AreaChart":{"signature":"AreaChart(labels: string[], series: Series[], variant?: \"linear\" | \"natural\" | \"step\", xLabel?: string, yLabel?: string, height?: number)","description":"Filled area under lines; use for cumulative totals or volume trends over time"},"RadarChart":{"signature":"RadarChart(labels: string[], series: Series[])","description":"Spider/web chart; use for comparing multiple variables across one or more entities"},"HorizontalBarChart":{"signature":"HorizontalBarChart(labels: string[], series: Series[], variant?: \"grouped\" | \"stacked\", xLabel?: string, yLabel?: string)","description":"Horizontal bars; prefer when category labels are long or for ranked lists"},"Series":{"signature":"Series(category: string, values: number[])","description":"One data series"},"PieChart":{"signature":"PieChart(labels: string[], values: number[], variant?: \"pie\" | \"donut\", appearance?: \"circular\" | \"semiCircular\")","description":"Circular slices; use plucked arrays: PieChart(data.categories, data.values)"},"RadialChart":{"signature":"RadialChart(labels: string[], values: number[])","description":"Radial bars; use plucked arrays: RadialChart(data.categories, data.values)"},"SingleStackedBarChart":{"signature":"SingleStackedBarChart(labels: string[], values: number[])","description":"Single horizontal stacked bar; use plucked arrays: SingleStackedBarChart(data.categories, data.values)"},"Slice":{"signature":"Slice(category: string, value: number)","description":"One slice with label and numeric value"},"ScatterChart":{"signature":"ScatterChart(datasets: ScatterSeries[], xLabel?: string, yLabel?: string)","description":"X/Y scatter plot; use for correlations, distributions, and clustering"},"ScatterSeries":{"signature":"ScatterSeries(name: string, points: Point[])","description":"Named dataset"},"Point":{"signature":"Point(x: number, y: number, z?: number)","description":"Data point with numeric coordinates"},"Form":{"signature":"Form(name: string, buttons: Buttons, fields?: FormControl[])","description":"Form container with fields and explicit action buttons"},"FormControl":{"signature":"FormControl(label: string, input: Input | TextArea | Select | DatePicker | Slider | CheckBoxGroup | RadioGroup | Chips | OptionCards, hint?: string)","description":"Field with label, input component, and optional hint text"},"Label":{"signature":"Label(text: string)","description":"Text label"},"Input":{"signature":"Input(name: string, placeholder?: string, type?: \"text\" | \"email\" | \"password\" | \"number\" | \"url\", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)","description":""},"TextArea":{"signature":"TextArea(name: string, placeholder?: string, rows?: number, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)","description":""},"Select":{"signature":"Select(name: string, items: SelectItem[], placeholder?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding, size?: \"small\" | \"medium\" | \"large\")","description":""},"SelectItem":{"signature":"SelectItem(value: string, label: string)","description":"Option for Select"},"DatePicker":{"signature":"DatePicker(name: string, mode?: \"single\" | \"range\", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)","description":""},"Slider":{"signature":"Slider(name: string, variant: \"continuous\" | \"discrete\", min: number, max: number, step?: number, defaultValue?: number[], label?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)","description":"Numeric slider input; supports continuous and discrete (stepped) variants"},"CheckBoxGroup":{"signature":"CheckBoxGroup(name: string, items: CheckBoxItem[], rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding>)","description":""},"CheckBoxItem":{"signature":"CheckBoxItem(label: string, description: string, name: string, defaultChecked?: boolean)","description":""},"RadioGroup":{"signature":"RadioGroup(name: string, items: RadioItem[], defaultValue?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)","description":""},"RadioItem":{"signature":"RadioItem(label: string, description: string, value: string)","description":""},"SwitchGroup":{"signature":"SwitchGroup(name: string, items: SwitchItem[], variant?: \"clear\" | \"card\" | \"sunk\", value?: $binding>)","description":"Group of switch toggles"},"SwitchItem":{"signature":"SwitchItem(label?: string, description?: string, name: string, defaultChecked?: boolean)","description":"Individual switch toggle"},"Button":{"signature":"Button(label: string, action?: ActionExpression, variant?: \"primary\" | \"secondary\" | \"tertiary\", type?: \"normal\" | \"destructive\", size?: \"extra-small\" | \"small\" | \"medium\" | \"large\")","description":"Clickable button"},"Buttons":{"signature":"Buttons(buttons: Button[], direction?: \"row\" | \"column\")","description":"Group of Button components. direction: \"row\" (default) | \"column\"."},"ListBlock":{"signature":"ListBlock(items: ListItem[], variant?: \"number\" | \"image\", size?: \"default\" | \"small\")","description":"A list of items with number or image indicators. Each item can optionally have an action. size small renders a compact list."},"ListItem":{"signature":"ListItem(title: string, subtitle?: string, image?: {src: string, alt: string}, actionLabel?: string, action?: ActionExpression)","description":"Item in a ListBlock — displays a title with an optional subtitle and image. When action is provided, the item becomes clickable."},"FollowUpBlock":{"signature":"FollowUpBlock(items: FollowUpItem[])","description":"List of clickable follow-up suggestions placed at the end of a response"},"FollowUpItem":{"signature":"FollowUpItem(text: string)","description":"Clickable follow-up suggestion — when clicked, sends text as user message"},"SectionBlock":{"signature":"SectionBlock(sections: SectionItem[], isFoldable?: boolean)","description":"Collapsible accordion sections. Auto-opens sections as they stream in. Use SectionItem for each section."},"SectionItem":{"signature":"SectionItem(value: string, trigger: string, content: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock | ListBlock | FollowUpBlock | Tabs | Accordion)[])","description":"Section with a label and collapsible content — used inside SectionBlock"},"Tabs":{"signature":"Tabs(items: TabItem[])","description":"Tabbed container"},"TabItem":{"signature":"TabItem(value: string, trigger: string, content: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock | ListBlock | FollowUpBlock | Accordion)[])","description":"value is unique id, trigger is tab label, content is array of components"},"Accordion":{"signature":"Accordion(items: AccordionItem[])","description":"Collapsible sections"},"AccordionItem":{"signature":"AccordionItem(value: string, trigger: string, content: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock | ListBlock | FollowUpBlock)[])","description":"value is unique id, trigger is section title"},"Steps":{"signature":"Steps(items: StepsItem[])","description":"Step-by-step guide"},"StepsItem":{"signature":"StepsItem(title: string, details: string)","description":"title and details text for one step"},"Carousel":{"signature":"Carousel(children: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock | ListBlock | FollowUpBlock)[][], variant?: \"card\" | \"sunk\")","description":"Horizontal scrollable carousel"},"TagBlock":{"signature":"TagBlock(tags: string[], size?: \"sm\" | \"md\" | \"lg\")","description":"tags is an array of strings; optional size sm | md | lg"},"Tag":{"signature":"Tag(text: string, icon?: Icon, size?: \"sm\" | \"md\" | \"lg\", variant?: \"neutral\" | \"info\" | \"success\" | \"warning\" | \"danger\")","description":"Styled tag/badge with optional Icon and variant"},"EntityList":{"signature":"EntityList(rows?: {left: string, right: string, rightVariant?: \"text\" | \"number\"}[], size?: \"small\" | \"default\", header?: {left: string, right: string, rightVariant?: \"text\" | \"number\"}, footer?: {left: string, right: string, rightVariant?: \"text\" | \"number\"})","description":"Two-column key/value rows (left label, right value). size 'default' supports optional header and footer rows; rightVariant 'number' uses tabular numbers."},"InlineHeader":{"signature":"InlineHeader(heading: string, description?: string)","description":"Compact section heading with an optional one-line description, for use inside cards."},"Icon":{"signature":"Icon(name: string, category?: string)","description":"A lucide icon by kebab-case name (e.g. 'circle-check'). Optional category picks a topical fallback when the name doesn't resolve."},"IconButton":{"signature":"IconButton(name: string, icon: Icon, action?: ActionExpression, variant?: \"primary\" | \"secondary\" | \"tertiary\", size?: \"extra-small\" | \"small\" | \"medium\" | \"large\", shape?: \"square\" | \"circle\")","description":"Icon-only button. name is the accessible label and the action label; icon is an Icon; action fires on click."},"EditableTable":{"signature":"EditableTable(name?: string, columns?: {type: \"text\" | \"number\" | \"date-single\" | \"select\" | \"url\", key?: string, header?: string, width?: number, options?: {value: string, label: string}[]}[], data?: {id: string, values: (string | number)[]}[])","description":"Spreadsheet-like table whose cells the user can edit inline (text, number, url, date, select columns); edits are saved back as a form field"},"ChipItem":{"signature":"ChipItem(value: string, label: string, icon?: Icon, disabled?: boolean)","description":"A single selectable chip inside a Chips group, with a value, label and optional icon."},"Chips":{"signature":"Chips(name: string, type?: \"single\" | \"multiple\", items?: ChipItem[], rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, defaultValue?: string | string[])","description":"A form field of compact selectable chips for choosing one or many short options; the selection is stored under `name`."},"OptionCard":{"signature":"OptionCard(value: string, title: string, subtitle?: string, topContent?: Icon | Image, disabled?: boolean)","description":"A single selectable card inside an OptionCards group, with a value, title, optional subtitle and an optional Icon or Image on top."},"OptionCards":{"signature":"OptionCards(name: string, type?: \"single\" | \"multiple\", items?: OptionCard[], rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, defaultValue?: string | string[])","description":"A form field of selectable cards (title, optional subtitle, optional icon or image) laid out in a responsive grid for choosing one or many options; the selection is stored under `name`."},"Text":{"signature":"Text(variant?: \"text\" | \"number\", value: string, subtext?: string, subtextVariant?: \"text\" | \"number\" | \"metric\", size?: \"xs\" | \"sm\" | \"md\" | \"lg\")","description":"Plain text line with optional subtext. variant 'number' uses tabular number styling; subtextVariant 'metric' colors a leading +/- subtext green/red."},"BoldText":{"signature":"BoldText(variant?: \"text\" | \"number\", value: string, subtext?: string, subtextVariant?: \"text\" | \"number\" | \"metric\", size?: \"xs\" | \"sm\" | \"md\" | \"lg\")","description":"Emphasized (bold) text line with optional subtext. variant 'number' uses tabular number styling; subtextVariant 'metric' colors a leading +/- subtext green/red."},"IconText":{"signature":"IconText(icon: Icon, iconVariant?: \"neutral\" | \"info\" | \"success\" | \"warning\" | \"danger\" | \"inverted\" | \"filled\" | \"soft\", iconSize?: \"xs\" | \"s\" | \"m\" | \"l\" | \"xl\" | \"sm\" | \"md\" | \"lg\", title: string, subtitle?: string, bold?: boolean, layout?: \"horizontal\" | \"vertical\")","description":"An icon badge with a title and optional subtitle, laid out horizontally or vertically. iconVariant sets the badge color."},"ImageText":{"signature":"ImageText(src: string, alt?: string, title: string, subtitle?: string, bold?: boolean, layout?: \"horizontal\" | \"vertical\", imageSize?: number)","description":"A small square image (thumbnail/avatar) with a title and optional subtitle. src must be a real image URL."},"ImageTextLarge":{"signature":"ImageTextLarge(src: string, alt?: string, title: string, subtitle?: string, bold?: boolean)","description":"A full-width banner image above a bold title and optional subtitle. src must be a real image URL."},"MetricIndicatorInline":{"signature":"MetricIndicatorInline(value: string, subtext?: string, trend?: {direction: \"up\" | \"down\", value: number})","description":"Headline metric value with an optional +/- percentage trend and subtext, all on one line."},"MetricIndicatorWithStrikethrough":{"signature":"MetricIndicatorWithStrikethrough(value: string, subtext?: string, previousValue?: string, trend?: {direction: \"up\" | \"down\", value: number})","description":"Headline metric value with an optional struck-through previousValue, a +/- percentage trend, and subtext below."},"SnippetCardItem":{"signature":"SnippetCardItem(id?: string, lhs: IconText | ImageText, rhs?: Text | BoldText)","description":"One row-style snippet card: a label on the left (IconText or ImageText) and an optional value on the right (Text or BoldText)."},"SnippetCardBlock":{"signature":"SnippetCardBlock(items: SnippetCardItem[], layout?: \"grid\", responsive?: boolean, action?: ActionExpression, gap?: number | string)","description":"A responsive grid of compact label/value cards (2 per row) for showing several short facts side by side; optionally clickable with a shared action."},"OverviewCardItem":{"signature":"OverviewCardItem(id?: string, top: IconText | ImageText | Text, bottom?: MetricIndicatorInline)","description":"One overview card: a heading slot at the top (IconText, ImageText or Text) and an optional MetricIndicatorInline at the bottom."},"OverviewCardBlock":{"signature":"OverviewCardBlock(items: OverviewCardItem[], layout?: \"grid\" | \"carousel\", responsive?: boolean, action?: ActionExpression, gap?: number | string)","description":"A grid or horizontal carousel of compact overview cards, each with a heading (icon/image/text) on top and an inline metric below; optionally clickable with a shared action."},"ContextCardItem":{"signature":"ContextCardItem(id?: string, title: string | Tag, body?: string, bgColor?: \"gray\", bgImageSrc?: string, bgImageAlt?: string)","description":"A single card inside a ContextCardBlock: a title (plain string or Tag), an optional markdown body, and an optional gray tint or background image."},"ContextCardBlock":{"signature":"ContextCardBlock(items: ContextCardItem[], layout?: \"grid\" | \"carousel\", responsive?: boolean, action?: ActionExpression, gap?: number | string)","description":"A grid or carousel of compact tinted context cards (title or tag plus a short bold body); an optional action makes every card clickable."},"CompositeCardItem":{"signature":"CompositeCardItem(id?: string, header?: IconText | ImageText | ImageTextLarge | Text | Image, body?: (Text | BoldText | MetricIndicatorInline | IconText | Image | AreaChart | BarChart | LineChart | ListBlock | TagBlock | EntityList)[], footer?: {price?: BoldText | MetricIndicatorWithStrikethrough, button?: Button})","description":"A single card inside a CompositeCardBlock: an optional header (icon/image/text), a stack of body elements (text, metrics, charts, lists, tags), and an optional price/button footer."},"CompositeCardBlock":{"signature":"CompositeCardBlock(items: CompositeCardItem[], layout?: \"grid\" | \"carousel\", responsive?: boolean, action?: ActionExpression, gap?: number | string)","description":"A two-per-row grid or carousel of rich cards, each with an optional header, stacked body content (text, metrics, charts, lists, tags) and a price/button footer; an optional action makes every card clickable."},"VisualCardItem":{"signature":"VisualCardItem(body: BoldText, id?: string, bgImageSrc?: string, tag?: Tag, bgImageAlt?: string)","description":"A single photo-first card inside a VisualCardBlock: a BoldText body panel, an optional Tag, and a background image (bgImageSrc must be a real URL; bgImageAlt is its alt text)."},"VisualCardBlock":{"signature":"VisualCardBlock(items: VisualCardItem[], layout?: \"grid\" | \"carousel\", responsive?: boolean, action?: ActionExpression, gap?: number | string)","description":"A grid or carousel of photo-first cards: a full-bleed background image with a tag on top and a bold text panel at the bottom; an optional action makes every card clickable."}}, +"componentGroups": [{"name":"Content","components":["CardHeader","TextContent","MarkDownRenderer","Callout","TextCallout","Image","ImageBlock","ImageGallery","CodeBlock","Separator","InlineHeader"],"notes":["- InlineHeader is a compact heading + description pair for labelling a block inside the response (lighter than CardHeader).","- Pass sources on Card ([{ title, sourceName, url }]) when the answer relies on references, and cite them inline in TextContent as [1], [2] (1-based index into sources). A Sources strip renders at the bottom of the card."]},{"name":"Tables","components":["Table","Col","EditableTable"],"notes":["- EditableTable lets the user edit cells inline. Give it a unique name, columns of { type, key, header } with type one of text | number | date-single | select | url (select also needs options: [{ value, label }]).","- data is an array of { id, values } rows where values are ordered positionally to match columns. Edited data is submitted when the user clicks Save Changes."]},{"name":"Charts (2D)","components":["BarChart","LineChart","AreaChart","RadarChart","HorizontalBarChart","Series"]},{"name":"Charts (1D)","components":["PieChart","RadialChart","SingleStackedBarChart","Slice"]},{"name":"Charts (Scatter)","components":["ScatterChart","ScatterSeries","Point"]},{"name":"Forms","components":["Form","FormControl","Label","Input","TextArea","Select","SelectItem","DatePicker","Slider","CheckBoxGroup","CheckBoxItem","RadioGroup","RadioItem","SwitchGroup","SwitchItem","Chips","ChipItem","OptionCards","OptionCard"],"notes":["- Chips: compact single/multiple selection pills. Use ChipItem references for each option.","- OptionCards: larger selectable cards with title, subtitle and an optional Icon or Image on top. Use OptionCard references for each option.","- Define EACH FormControl as its own reference — do NOT inline all controls in one array.","- NEVER nest Form inside Form.","- Form requires explicit buttons. Always pass a Buttons(...) reference as the second Form argument: Form(name, buttons, fields).","- rules is an optional object: { required: true, email: true, min: 8, maxLength: 100 }","- The renderer shows error messages automatically — do NOT generate error text in the UI"]},{"name":"Buttons","components":["Button","Buttons","Icon","IconButton"],"notes":["- Icon renders a lucide icon by kebab-case name; it is also used as the icon of IconButton, IconText and OptionCard."]},{"name":"Lists & Follow-ups","components":["ListBlock","ListItem","FollowUpBlock","FollowUpItem"],"notes":["- Use ListBlock with ListItem references for numbered lists.","- Use FollowUpBlock with FollowUpItem references at the end of a response to suggest next actions.","- A ListItem is clickable ONLY when given an action (5th argument); without one it is plain text.","- Clicking a FollowUpItem, or a ListItem with a continue_conversation action, sends text to the LLM as a user message.","- Example: list = ListBlock([item1, item2]) item1 = ListItem(\"Option A\", \"Details about A\", null, null, { type: \"continue_conversation\", context: \"Option A\" })"]},{"name":"Sections","components":["SectionBlock","SectionItem"],"notes":["- SectionBlock renders collapsible accordion sections that auto-open as they stream.","- Each section needs a unique `value` id, a `trigger` label, and a `content` array.","- Example: sections = SectionBlock([s1, s2]) s1 = SectionItem(\"intro\", \"Introduction\", [content1])","- Set isFoldable=false to render sections as flat headers instead of accordion."]},{"name":"Layout","components":["Tabs","TabItem","Accordion","AccordionItem","Steps","StepsItem","Carousel"],"notes":["- Use Tabs to present alternative views — each TabItem has a value id, trigger label, and content array.","- Carousel takes an array of slides, where each slide is an array of content: carousel = Carousel([[t1, img1], [t2, img2]])","- IMPORTANT: Every slide in a Carousel must have the same structure — same component types in the same order.","- For image carousels use: [[title, image, description, tags], ...] — every slide must follow this exact pattern.","- Use real, publicly accessible image URLs (e.g. https://picsum.photos/seed/KEYWORD/800/500). Never hallucinate image URLs."]},{"name":"Data Display","components":["TagBlock","Tag","EntityList"],"notes":["- EntityList is a compact two-column list of { left, right } rows (e.g. name / value). size='default' also supports a header and footer row; size='small' does not."]},{"name":"Cards","components":["SnippetCardBlock","SnippetCardItem","OverviewCardBlock","OverviewCardItem","ContextCardBlock","ContextCardItem","CompositeCardBlock","CompositeCardItem","VisualCardBlock","VisualCardItem","Text","BoldText","IconText","ImageText","ImageTextLarge","MetricIndicatorInline","MetricIndicatorWithStrikethrough"],"notes":["- Card blocks lay out 2+ items in a responsive grid (or carousel where supported). Every item in a block must have the same structure.","- SnippetCardItem: small card with lhs (IconText | ImageText) and optional rhs (Text | BoldText) — good for key/value facts.","- OverviewCardItem: small card with top (IconText | ImageText | Text) and optional bottom MetricIndicatorInline — good for KPIs.","- ContextCardItem: medium card with a title (string or Tag), body text and optional background image — good for summaries.","- CompositeCardItem: rich card with header, body array (Text, BoldText, MetricIndicatorInline, IconText, Image, charts, ListBlock, TagBlock, EntityList) and footer (price + Button) — good for products/offers.","- VisualCardItem: image-first card with a BoldText body and optional Tag.","- Text / BoldText / IconText / ImageText / ImageTextLarge / MetricIndicator* are the inline building blocks used INSIDE card items; do not place them directly in the root Card."]}] +} diff --git a/packages/swift-lang/Tests/OpenUILangTests/Fixtures/cloud.json b/packages/swift-lang/Tests/OpenUILangTests/Fixtures/cloud.json new file mode 100644 index 0000000000..15077f950e --- /dev/null +++ b/packages/swift-lang/Tests/OpenUILangTests/Fixtures/cloud.json @@ -0,0 +1,4 @@ +{ +"cases": [{"name":"built-in","spec":{},"expected":"]]>openui:config\n{\"libraryVersion\":\"0.1.0\"}"},{"name":"built-in-instructions","spec":{"instructions":"Answer in French."},"expected":"]]>openui:config\n{\"libraryVersion\":\"0.1.0\"}\nAnswer in French."},{"name":"built-in-empty-options","spec":{"promptOptions":{"preamble":""}},"expected":"]]>openui:config\n{\"libraryVersion\":\"0.1.0\"}"},{"name":"built-in-with-options","spec":{"promptOptions":{"examples":[]}},"error":"[generateSystemPrompt] promptOptions requires a library — the built-in library ignores it."},{"name":"library","spec":{"library":{"root":"Card","components":{"Card":{"signature":"Card(children: Text[])"}},"componentGroups":[{"name":"Layout","components":["Card","Text"],"notes":["- Note"]}],"schema":{"$defs":{"Card":{"properties":{"children":{"type":"array","items":{"$ref":"#/$defs/Text"}}},"required":["children"]},"Text":{"properties":{"value":{"type":"string"}},"required":["value"]}}}}},"expected":"]]>openui:config\n{\"chatLibrary\":{\"root\":\"Card\",\"componentGroups\":[{\"name\":\"Layout\",\"components\":[\"Card\",\"Text\"],\"notes\":[\"- Note\"]}],\"schema\":{\"$defs\":{\"Card\":{\"properties\":{\"children\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/$defs/Text\"}}},\"required\":[\"children\"]},\"Text\":{\"properties\":{\"value\":{\"type\":\"string\"}},\"required\":[\"value\"]}}}}}"},{"name":"library-options","spec":{"library":{"root":"Card","components":{"Card":{"signature":"Card(children: Text[])"}},"componentGroups":[{"name":"Layout","components":["Card","Text"],"notes":["- Note"]}],"schema":{"$defs":{"Card":{"properties":{"children":{"type":"array","items":{"$ref":"#/$defs/Text"}}},"required":["children"]},"Text":{"properties":{"value":{"type":"string"}},"required":["value"]}}}},"promptOptions":{"preamble":"Be brief.","additionalRules":["One card"],"examples":["a = 1"],"tools":["x"],"editMode":true},"instructions":"Extra."},"expected":"]]>openui:config\n{\"chatLibrary\":{\"root\":\"Card\",\"componentGroups\":[{\"name\":\"Layout\",\"components\":[\"Card\",\"Text\"],\"notes\":[\"- Note\"]}],\"schema\":{\"$defs\":{\"Card\":{\"properties\":{\"children\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/$defs/Text\"}}},\"required\":[\"children\"]},\"Text\":{\"properties\":{\"value\":{\"type\":\"string\"}},\"required\":[\"value\"]}}}},\"systemPromptOptions\":{\"examples\":[\"a = 1\"],\"preamble\":\"Be brief.\",\"additionalRules\":[\"One card\"]}}\nExtra."},{"name":"library-empty-strings","spec":{"library":{"root":"Card","components":{"Card":{"signature":"Card(children: Text[])"}},"componentGroups":[{"name":"Layout","components":["Card","Text"],"notes":["- Note"]}],"schema":{"$defs":{"Card":{"properties":{"children":{"type":"array","items":{"$ref":"#/$defs/Text"}}},"required":["children"]},"Text":{"properties":{"value":{"type":"string"}},"required":["value"]}}}},"promptOptions":{"preamble":""},"instructions":""},"expected":"]]>openui:config\n{\"chatLibrary\":{\"root\":\"Card\",\"componentGroups\":[{\"name\":\"Layout\",\"components\":[\"Card\",\"Text\"],\"notes\":[\"- Note\"]}],\"schema\":{\"$defs\":{\"Card\":{\"properties\":{\"children\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/$defs/Text\"}}},\"required\":[\"children\"]},\"Text\":{\"properties\":{\"value\":{\"type\":\"string\"}},\"required\":[\"value\"]}}}}}"},{"name":"library-without-root","spec":{"library":{"components":{"Card":{"signature":"Card(children: Text[])"}},"schema":{"$defs":{"Card":{"properties":{"children":{"type":"array","items":{"$ref":"#/$defs/Text"}}},"required":["children"]},"Text":{"properties":{"value":{"type":"string"}},"required":["value"]}}}}},"expected":"]]>openui:config\n{\"chatLibrary\":{\"schema\":{\"$defs\":{\"Card\":{\"properties\":{\"children\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/$defs/Text\"}}},\"required\":[\"children\"]},\"Text\":{\"properties\":{\"value\":{\"type\":\"string\"}},\"required\":[\"value\"]}}}}}"},{"name":"library-with-id","spec":{"library":{"root":"Card","components":{"Card":{"signature":"Card(children: Text[])"}},"componentGroups":[{"name":"Layout","components":["Card","Text"],"notes":["- Note"]}],"schema":{"$defs":{"Card":{"properties":{"children":{"type":"array","items":{"$ref":"#/$defs/Text"}}},"required":["children"]},"Text":{"properties":{"value":{"type":"string"}},"required":["value"]}}},"id":"lib-1"}},"expected":"]]>openui:config\n{\"chatLibrary\":{\"root\":\"Card\",\"componentGroups\":[{\"name\":\"Layout\",\"components\":[\"Card\",\"Text\"],\"notes\":[\"- Note\"]}],\"schema\":{\"$defs\":{\"Card\":{\"properties\":{\"children\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/$defs/Text\"}}},\"required\":[\"children\"]},\"Text\":{\"properties\":{\"value\":{\"type\":\"string\"}},\"required\":[\"value\"]}}},\"id\":\"lib-1\"}}"},{"name":"broken-library","spec":{"library":{"root":"Missing","components":{},"componentGroups":[{"name":"G","components":["Card","Nope"]},{"name":1}],"schema":{"$defs":{"Card":{"properties":{"children":{"type":"array","items":{"$ref":"#/$defs/Gone"}},"x":{"$ref":"bad"}},"required":["children","absent",3]},"Text":{"properties":[]},"Bad":"nope"}}}},"error":"[generateSystemPrompt] Invalid library: Root component \"Missing\" was not found in schema.$defs. Available components: Card, Text, Bad. $defs/Card/required must be an array of property names. Unresolvable $ref \"#/$defs/Gone\" at $defs/Card/properties/children/items — refs must be \"#/$defs/\" pointing at a component in $defs. Unresolvable $ref \"bad\" at $defs/Card/properties/x — refs must be \"#/$defs/\" pointing at a component in $defs. $defs/Text/properties must be an object. $defs/Bad must be an object component schema. Component group \"G\" references unknown component \"Nope\". Each componentGroups entry must be {name: string, components: string[], notes?: string[]}."},{"name":"no-defs","spec":{"library":{"components":{},"schema":{"$defs":{}}}},"error":"[generateSystemPrompt] Invalid library: chatLibrary.schema.$defs must be a non-empty object keyed by component name."},{"name":"no-schema","spec":{"library":{"root":"","components":{}}},"error":"[generateSystemPrompt] Invalid library: chatLibrary.root, when present, must be a non-empty string. chatLibrary.schema must be an object with a $defs map of component schemas."}], +"chat": {"length":83747,"sha256":"57e211d92850453a1be46c1e7653d40ec0714b309cef203f4eba589dde7e4f3e"} +} diff --git a/packages/swift-lang/Tests/OpenUILangTests/Fixtures/errors.json b/packages/swift-lang/Tests/OpenUILangTests/Fixtures/errors.json new file mode 100644 index 0000000000..742eb95cee --- /dev/null +++ b/packages/swift-lang/Tests/OpenUILangTests/Fixtures/errors.json @@ -0,0 +1,9 @@ +[ +{"name":"unknown-component","input":"root = Card([Mystery(\"a\"), TextContent(\"b\")])","expected":[{"source":"parser","code":"unknown-component","message":"Unknown component \"Mystery\" — not found in catalog or builtins. Available components: Card, TextContent, MarkDownRenderer, CardHeader, Callout, TextCallout, CodeBlock, Image, ImageBlock, ImageGallery, Separator, HorizontalBarChart, Series, RadarChart, PieChart, RadialChart, SingleStackedBarChart, ScatterChart, ScatterSeries, Point, AreaChart, BarChart, LineChart, Table, Col, TagBlock, Form, Buttons, Button, FormControl, Input, TextArea, Select, SelectItem, DatePicker, Slider, CheckBoxGroup, CheckBoxItem, RadioGroup, RadioItem, Chips, ChipItem, Icon, OptionCards, OptionCard, IconButton, Steps, StepsItem, InlineHeader, EntityList, EditableTable, SnippetCardBlock, SnippetCardItem, IconText, ImageText, Text, BoldText, OverviewCardBlock, OverviewCardItem, MetricIndicatorInline, ContextCardBlock, ContextCardItem, Tag, CompositeCardBlock, CompositeCardItem, ImageTextLarge, ListBlock, ListItem, MetricIndicatorWithStrikethrough, VisualCardBlock, VisualCardItem, FollowUpBlock, FollowUpItem, SectionBlock, SectionItem, Tabs, TabItem, Accordion, AccordionItem, Carousel, Slice, Label, SwitchGroup, SwitchItem","component":"Mystery","statementId":"root","hint":"Available components: Card, CardHeader, TextContent, MarkDownRenderer, Callout, TextCallout, Image, ImageBlock, ImageGallery, CodeBlock, Separator, Table, Col, BarChart, LineChart, AreaChart, RadarChart, HorizontalBarChart, Series, PieChart, RadialChart, SingleStackedBarChart, Slice, ScatterChart, ScatterSeries, Point, Form, FormControl, Label, Input, TextArea, Select, SelectItem, DatePicker, Slider, CheckBoxGroup, CheckBoxItem, RadioGroup, RadioItem, SwitchGroup, SwitchItem, Button, Buttons, ListBlock, ListItem, FollowUpBlock, FollowUpItem, SectionBlock, SectionItem, Tabs, TabItem, Accordion, AccordionItem, Steps, StepsItem, Carousel, TagBlock, Tag, EntityList, InlineHeader, Icon, IconButton, EditableTable, ChipItem, Chips, OptionCard, OptionCards, Text, BoldText, IconText, ImageText, ImageTextLarge, MetricIndicatorInline, MetricIndicatorWithStrikethrough, SnippetCardItem, SnippetCardBlock, OverviewCardItem, OverviewCardBlock, ContextCardItem, ContextCardBlock, CompositeCardItem, CompositeCardBlock, VisualCardItem, VisualCardBlock"}]}, +{"name":"missing-required","input":"root = Card([TextContent()])","expected":[{"source":"parser","code":"missing-required","message":"missing required field \"/text\" — signature: TextContent(text*: string, size: \"small\"|\"default\"|\"large\"|\"small-heavy\"|\"large-heavy\")","component":"TextContent","path":"/text","statementId":"root","hint":"Signature: TextContent(text*, size) — * marks required"}]}, +{"name":"null-required","input":"root = Card([TextContent(null)])","expected":[{"source":"parser","code":"null-required","message":"required field \"/text\" cannot be null — signature: TextContent(text*: string, size: \"small\"|\"default\"|\"large\"|\"small-heavy\"|\"large-heavy\")","component":"TextContent","path":"/text","statementId":"root","hint":"Signature: TextContent(text*, size) — * marks required"}]}, +{"name":"inline-reserved","input":"root = Card([TextContent(Query(\"get\", {}))])","expected":[{"source":"parser","code":"inline-reserved","message":"Query() must be declared as a top-level statement, not used inline as a value","component":"Query","statementId":"root","hint":"Declare as a top-level statement: myVar = Query(...)"},{"source":"parser","code":"null-required","message":"required field \"/text\" cannot be null — signature: TextContent(text*: string, size: \"small\"|\"default\"|\"large\"|\"small-heavy\"|\"large-heavy\")","component":"TextContent","path":"/text","statementId":"root","hint":"Signature: TextContent(text*, size) — * marks required"}]}, +{"name":"excess-args","input":"root = Card([CardHeader(\"a\", \"b\", \"c\", \"d\")])","expected":[{"source":"parser","code":"excess-args","message":"CardHeader takes 2 arg(s), got 4 (2 excess dropped)","component":"CardHeader","statementId":"root"}]}, +{"name":"type-mismatch","input":"root = Card([TextContent(42)])","expected":[{"source":"parser","code":"type-mismatch","message":"field \"/text\" expects string but got number","component":"TextContent","path":"/text","statementId":"root"}]}, +{"name":"several","input":"root = Card([a, b])\na = Foo()\nb = Button()","expected":[{"source":"parser","code":"unknown-component","message":"Unknown component \"Foo\" — not found in catalog or builtins. Available components: Card, TextContent, MarkDownRenderer, CardHeader, Callout, TextCallout, CodeBlock, Image, ImageBlock, ImageGallery, Separator, HorizontalBarChart, Series, RadarChart, PieChart, RadialChart, SingleStackedBarChart, ScatterChart, ScatterSeries, Point, AreaChart, BarChart, LineChart, Table, Col, TagBlock, Form, Buttons, Button, FormControl, Input, TextArea, Select, SelectItem, DatePicker, Slider, CheckBoxGroup, CheckBoxItem, RadioGroup, RadioItem, Chips, ChipItem, Icon, OptionCards, OptionCard, IconButton, Steps, StepsItem, InlineHeader, EntityList, EditableTable, SnippetCardBlock, SnippetCardItem, IconText, ImageText, Text, BoldText, OverviewCardBlock, OverviewCardItem, MetricIndicatorInline, ContextCardBlock, ContextCardItem, Tag, CompositeCardBlock, CompositeCardItem, ImageTextLarge, ListBlock, ListItem, MetricIndicatorWithStrikethrough, VisualCardBlock, VisualCardItem, FollowUpBlock, FollowUpItem, SectionBlock, SectionItem, Tabs, TabItem, Accordion, AccordionItem, Carousel, Slice, Label, SwitchGroup, SwitchItem","component":"Foo","statementId":"a","hint":"Available components: Card, CardHeader, TextContent, MarkDownRenderer, Callout, TextCallout, Image, ImageBlock, ImageGallery, CodeBlock, Separator, Table, Col, BarChart, LineChart, AreaChart, RadarChart, HorizontalBarChart, Series, PieChart, RadialChart, SingleStackedBarChart, Slice, ScatterChart, ScatterSeries, Point, Form, FormControl, Label, Input, TextArea, Select, SelectItem, DatePicker, Slider, CheckBoxGroup, CheckBoxItem, RadioGroup, RadioItem, SwitchGroup, SwitchItem, Button, Buttons, ListBlock, ListItem, FollowUpBlock, FollowUpItem, SectionBlock, SectionItem, Tabs, TabItem, Accordion, AccordionItem, Steps, StepsItem, Carousel, TagBlock, Tag, EntityList, InlineHeader, Icon, IconButton, EditableTable, ChipItem, Chips, OptionCard, OptionCards, Text, BoldText, IconText, ImageText, ImageTextLarge, MetricIndicatorInline, MetricIndicatorWithStrikethrough, SnippetCardItem, SnippetCardBlock, OverviewCardItem, OverviewCardBlock, ContextCardItem, ContextCardBlock, CompositeCardItem, CompositeCardBlock, VisualCardItem, VisualCardBlock"},{"source":"parser","code":"missing-required","message":"missing required field \"/label\" — signature: Button(label*: string, action, variant: \"primary\"|\"secondary\"|\"tertiary\", type: \"normal\"|\"destructive\", size: \"extra-small\"|\"small\"|\"medium\"|\"large\")","component":"Button","path":"/label","statementId":"b","hint":"Signature: Button(label*, action, variant, type, size) — * marks required"}]} +] diff --git a/packages/swift-lang/Tests/OpenUILangTests/Fixtures/evaluation.json b/packages/swift-lang/Tests/OpenUILangTests/Fixtures/evaluation.json new file mode 100644 index 0000000000..d1aeb92666 --- /dev/null +++ b/packages/swift-lang/Tests/OpenUILangTests/Fixtures/evaluation.json @@ -0,0 +1,27 @@ +[ +{"name":"chat-example-1","schema":"chat","input":"Example 1 — Informational response (the shape for \"tell me about X\" / explainer asks):\n\nroot = Card([header, intro, highlights, statsHeader, statsCards, sectionsBlock, actionBtns], [src1, src2])\nheader = CardHeader(\"Paris\", \"The City of Light · France's Eternal Capital\")\nintro = TextContent(\"Paris is one of the world's most visited cities — a timeless blend of iconic landmarks, world-class cuisine, art, and romance [1]. In 2026 the city is buzzing with new energy: the **Grand Palais** has reopened after a €466M renovation, and you can now **swim in the Seine** for the first time in over a century [2].\")\nhighlights = ContextCardBlock([h1, h2, h3], \"grid\", true, { type: \"continue_conversation\", context: \"Tell me more about this aspect of Paris\" })\nh1 = ContextCardItem(\"art-museums\", \"Art & Museums\", \"143 museums including the Louvre & newly reopened Grand Palais.\", \"gray\", \"https://cdn.britannica.com/03/121003-050-2544BD4E/Interior-Louvre-Museum-Paris.jpg\")\nh2 = ContextCardItem(\"gastronomy\", \"Gastronomy\", \"9,000+ restaurants, 130+ Michelin stars, and the world's best baguettes.\", \"gray\")\nh3 = ContextCardItem(\"fashion-capital\", \"Fashion Capital\", \"Home to Chanel, Dior, Louis Vuitton & Hermès — the global luxury hub.\", \"gray\", \"https://thumbs.dreamstime.com/z/dior-storefront-facade-chanel-store-lvmh-s-french-designer-modehouse-christian-adjacent-to-luxury-fashion-beauty-brand-327161553.jpg\")\nstatsHeader = InlineHeader(\"Paris by the Numbers\", \"Key visitor statistics\")\nstatsCards = OverviewCardBlock([ov1, ov2, ov3], \"grid\", true)\nov1 = OverviewCardItem(\"annual-visitors\", ov1top, ov1metric)\nov1top = IconText(ov1icon, \"neutral\", \"m\", \"Annual Visitors\", \"2024 arrivals\", false, \"vertical\")\nov1icon = Icon(\"users\", \"people\")\nov1metric = MetricIndicatorInline(\"48.7M\", \"tourists\", { direction: \"up\", value: 2.5 })\nov2 = OverviewCardItem(\"museums-monuments\", ov2top, ov2metric)\nov2top = IconText(ov2icon, \"neutral\", \"m\", \"Museums & Monuments\", \"Cultural sites\", false, \"vertical\")\nov2icon = Icon(\"landmark\", \"buildings\")\nov2metric = MetricIndicatorInline(\"2,370+\", \"across the city\")\nov3 = OverviewCardItem(\"tourism-revenue\", ov3top, ov3metric)\nov3top = IconText(ov3icon, \"neutral\", \"m\", \"Tourism Revenue\", \"2024 estimate\", false, \"vertical\")\nov3icon = Icon(\"circle-dollar-sign\", \"finance\")\nov3metric = MetricIndicatorInline(\"€23.4B\", \"generated\")\nsectionsBlock = SectionBlock([secLandmarks, secFood, secWhen], false)\nsecLandmarks = SectionItem(\"landmarks\", \"Iconic Landmarks\", [landmarksCarousel])\nlandmarksCarousel = VisualCardBlock([lm1, lm2, lm3, lm4], \"carousel\", true, { type: \"continue_conversation\", context: \"Tell me more about this Paris landmark\" })\nlm1 = VisualCardItem(lm1body, \"eiffel-tower\", \"https://c8.alamy.com/comp/RYEB13/aerial-view-of-the-eiffel-tower-with-the-park-champ-de-mars-and-the-river-seine-paris-france-RYEB13.jpg\", lm1tag, \"Aerial view of the Eiffel Tower\")\nlm1body = BoldText(\"text\", \"Eiffel Tower\", \"Open daily 9am–midnight\")\nlm1tag = Tag(\"Must-See\", lm1tagIcon, \"sm\", \"info\")\nlm1tagIcon = Icon(\"star\", \"shapes\")\nlm2 = VisualCardItem(lm2body, \"the-louvre\", \"https://images.fineartamerica.com/images/artworkimages/mediumlarge/3/1-exterior-of-musee-du-louvre-museum-paris-france-bernard-jaubert.jpg\", lm2tag, \"Exterior of the Louvre museum\")\nlm2body = BoldText(\"text\", \"The Louvre\", \"World's largest art museum\")\nlm2tag = Tag(\"8.9M visits/yr\", lm2tagIcon, \"sm\", \"success\")\nlm2tagIcon = Icon(\"trending-up\", \"charts\")\nlm3 = VisualCardItem(lm3body, \"notre-dame\", \"https://cdn.britannica.com/29/255529-050-63A22A3C/notre-dame-de-paris-cathedral-paris-france.jpg\", lm3tag, \"Notre-Dame cathedral facade\")\nlm3body = BoldText(\"text\", \"Notre-Dame\", \"Fully restored cathedral\")\nlm3tag = Tag(\"Reopened 2024\", lm3tagIcon, \"sm\", \"warning\")\nlm3tagIcon = Icon(\"hammer\", \"tools\")\nlm4 = VisualCardItem(lm4body, \"versailles\", \"https://cdn.britannica.com/76/130076-050-31128A3D/Latona-Fountain-Balthazard-Marsy-Gaspard-Andre-Le.jpg\", lm4tag, \"Latona Fountain at Versailles\")\nlm4body = BoldText(\"text\", \"Palace of Versailles\", \"Royal gardens & grand halls\")\nlm4tag = Tag(\"Day Trip\", lm4tagIcon, \"sm\", \"neutral\")\nlm4tagIcon = Icon(\"train-front\", \"travel\")\nsecFood = SectionItem(\"food\", \"Food & Drink\", [foodCards])\nfoodCards = ContextCardBlock([f1, f2, f3], \"grid\", true, { type: \"continue_conversation\", context: \"Tell me more about eating and drinking in Paris\" })\nf1 = ContextCardItem(\"bistros\", \"Bistros\", \"Classic French fare — try Les Arlots for confit beef cheeks.\", \"gray\", \"https://everydayparisian.com/wp-content/uploads/2023/01/IMG_0221-768x1024.webp\")\nf2 = ContextCardItem(\"fine-dining\", \"Fine Dining\", \"Septime leads a new wave of creative Michelin-starred cuisine.\", \"gray\", \"https://production-data.worldofmouth.app/images/72b2e6db-97ea-49c4-aa5d-3b7c8121c812.jpg\")\nf3 = ContextCardItem(\"bakeries\", \"Bakeries\", \"Shinya Pain for sourdough. Grand Prix-winning baguettes citywide.\", \"gray\", \"https://www.davidlebovitz.com/wp-content/uploads/2018/09/Le-petit-grain-paris-bakery-boulangerie-patisserie-pastry-shop-8-640x895.jpg\")\nsecWhen = SectionItem(\"when\", \"Best Time to Visit\", [whenTabs])\nwhenTabs = Tabs([tabSpring, tabSummer])\ntabSpring = TabItem(\"spring\", \"Spring\", [springList])\nspringList = ListBlock([sp1, sp2, sp3], \"number\")\nsp1 = ListItem(\"Cherry blossoms at Parc de Sceaux\", \"March – May\", null, null, { type: \"continue_conversation\", context: \"Tell me about cherry blossom season in Paris\" })\nsp2 = ListItem(\"Picnics along the Seine & in city parks\", \"Warm, lively atmosphere\", null, null, { type: \"continue_conversation\", context: \"Where are the best picnic spots in Paris?\" })\nsp3 = ListItem(\"Fewer crowds than summer\", \"Ideal for sightseeing\", null, null, { type: \"continue_conversation\", context: \"How busy is Paris in spring?\" })\ntabSummer = TabItem(\"summer\", \"Summer\", [summerList])\nsummerList = ListBlock([su1, su2, su3], \"number\")\nsu1 = ListItem(\"Swim in the Seine — open July & August 2026\", \"First time in 102 years\", null, null, { type: \"continue_conversation\", context: \"Tell me about swimming in the Seine\" })\nsu2 = ListItem(\"Open-air cinema at Parc de la Villette\", \"Free films under the stars\", null, null, { type: \"continue_conversation\", context: \"Tell me about the open-air cinema at Parc de la Villette\" })\nsu3 = ListItem(\"Bastille Day celebrations on July 14\", \"Fireworks at the Eiffel Tower\", null, null, { type: \"continue_conversation\", context: \"What happens in Paris on Bastille Day?\" })\nactionBtns = Buttons([btn1, btn2, btn3], \"row\")\nbtn1 = Button(\"Plan a Trip to Paris\", { type: \"continue_conversation\", context: \"Help me plan a trip to Paris\" }, \"primary\")\nbtn2 = Button(\"Best Hotels in Paris\", { type: \"continue_conversation\", context: \"Show me the best hotels in Paris\" }, \"secondary\")\nbtn3 = Button(\"Build a Paris Itinerary\", { type: \"continue_conversation\", context: \"Build a 5-day Paris itinerary for me\" }, \"secondary\")\nsrc1 = { title: \"Paris Travel Guide 2025\", sourceName: \"Travel and Tour World\", url: \"https://www.travelandtourworld.com/news/article/from-iconic-landmarks-to-hidden-gems-the-ultimate-paris-travel-guide-for-2025-you-need-to-read-now/\" }\nsrc2 = { title: \"Best Things to Do in Paris 2026\", sourceName: \"Time Out Paris\", url: \"http://www.timeout.fr/paris/en/for-tourists\" }","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Paris","subtitle":"The City of Light · France's Eternal Capital"},"partial":false,"hasDynamicProps":false,"statementId":"header"},{"type":"element","typeName":"TextContent","props":{"text":"Paris is one of the world's most visited cities — a timeless blend of iconic landmarks, world-class cuisine, art, and romance [1]. In 2026 the city is buzzing with new energy: the **Grand Palais** has reopened after a €466M renovation, and you can now **swim in the Seine** for the first time in over a century [2]."},"partial":false,"hasDynamicProps":false,"statementId":"intro"},{"type":"element","typeName":"ContextCardBlock","props":{"items":[{"type":"element","typeName":"ContextCardItem","props":{"id":"art-museums","title":"Art & Museums","body":"143 museums including the Louvre & newly reopened Grand Palais.","bgColor":"gray","bgImageSrc":"https://cdn.britannica.com/03/121003-050-2544BD4E/Interior-Louvre-Museum-Paris.jpg"},"partial":false,"hasDynamicProps":false,"statementId":"h1"},{"type":"element","typeName":"ContextCardItem","props":{"id":"gastronomy","title":"Gastronomy","body":"9,000+ restaurants, 130+ Michelin stars, and the world's best baguettes.","bgColor":"gray"},"partial":false,"hasDynamicProps":false,"statementId":"h2"},{"type":"element","typeName":"ContextCardItem","props":{"id":"fashion-capital","title":"Fashion Capital","body":"Home to Chanel, Dior, Louis Vuitton & Hermès — the global luxury hub.","bgColor":"gray","bgImageSrc":"https://thumbs.dreamstime.com/z/dior-storefront-facade-chanel-store-lvmh-s-french-designer-modehouse-christian-adjacent-to-luxury-fashion-beauty-brand-327161553.jpg"},"partial":false,"hasDynamicProps":false,"statementId":"h3"}],"layout":"grid","responsive":true,"action":{"type":"continue_conversation","context":"Tell me more about this aspect of Paris"}},"partial":false,"hasDynamicProps":false,"statementId":"highlights"},{"type":"element","typeName":"InlineHeader","props":{"heading":"Paris by the Numbers","description":"Key visitor statistics"},"partial":false,"hasDynamicProps":false,"statementId":"statsHeader"},{"type":"element","typeName":"OverviewCardBlock","props":{"items":[{"type":"element","typeName":"OverviewCardItem","props":{"id":"annual-visitors","top":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"users","category":"people"},"partial":false,"hasDynamicProps":false,"statementId":"ov1icon"},"iconVariant":"neutral","iconSize":"m","title":"Annual Visitors","subtitle":"2024 arrivals","bold":false,"layout":"vertical"},"partial":false,"hasDynamicProps":false,"statementId":"ov1top"},"bottom":{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"48.7M","subtext":"tourists","trend":{"direction":"up","value":2.5}},"partial":false,"hasDynamicProps":false,"statementId":"ov1metric"}},"partial":false,"hasDynamicProps":false,"statementId":"ov1"},{"type":"element","typeName":"OverviewCardItem","props":{"id":"museums-monuments","top":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"landmark","category":"buildings"},"partial":false,"hasDynamicProps":false,"statementId":"ov2icon"},"iconVariant":"neutral","iconSize":"m","title":"Museums & Monuments","subtitle":"Cultural sites","bold":false,"layout":"vertical"},"partial":false,"hasDynamicProps":false,"statementId":"ov2top"},"bottom":{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"2,370+","subtext":"across the city"},"partial":false,"hasDynamicProps":false,"statementId":"ov2metric"}},"partial":false,"hasDynamicProps":false,"statementId":"ov2"},{"type":"element","typeName":"OverviewCardItem","props":{"id":"tourism-revenue","top":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"circle-dollar-sign","category":"finance"},"partial":false,"hasDynamicProps":false,"statementId":"ov3icon"},"iconVariant":"neutral","iconSize":"m","title":"Tourism Revenue","subtitle":"2024 estimate","bold":false,"layout":"vertical"},"partial":false,"hasDynamicProps":false,"statementId":"ov3top"},"bottom":{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"€23.4B","subtext":"generated"},"partial":false,"hasDynamicProps":false,"statementId":"ov3metric"}},"partial":false,"hasDynamicProps":false,"statementId":"ov3"}],"layout":"grid","responsive":true},"partial":false,"hasDynamicProps":false,"statementId":"statsCards"},{"type":"element","typeName":"SectionBlock","props":{"sections":[{"type":"element","typeName":"SectionItem","props":{"value":"landmarks","trigger":"Iconic Landmarks","content":[{"type":"element","typeName":"VisualCardBlock","props":{"items":[{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Eiffel Tower","subtext":"Open daily 9am–midnight","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"lm1body"},"id":"eiffel-tower","bgImageSrc":"https://c8.alamy.com/comp/RYEB13/aerial-view-of-the-eiffel-tower-with-the-park-champ-de-mars-and-the-river-seine-paris-france-RYEB13.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Must-See","icon":{"type":"element","typeName":"Icon","props":{"name":"star","category":"shapes"},"partial":false,"hasDynamicProps":false,"statementId":"lm1tagIcon"},"size":"sm","variant":"info"},"partial":false,"hasDynamicProps":false,"statementId":"lm1tag"},"bgImageAlt":"Aerial view of the Eiffel Tower"},"partial":false,"hasDynamicProps":false,"statementId":"lm1"},{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"The Louvre","subtext":"World's largest art museum","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"lm2body"},"id":"the-louvre","bgImageSrc":"https://images.fineartamerica.com/images/artworkimages/mediumlarge/3/1-exterior-of-musee-du-louvre-museum-paris-france-bernard-jaubert.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"8.9M visits/yr","icon":{"type":"element","typeName":"Icon","props":{"name":"trending-up","category":"charts"},"partial":false,"hasDynamicProps":false,"statementId":"lm2tagIcon"},"size":"sm","variant":"success"},"partial":false,"hasDynamicProps":false,"statementId":"lm2tag"},"bgImageAlt":"Exterior of the Louvre museum"},"partial":false,"hasDynamicProps":false,"statementId":"lm2"},{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Notre-Dame","subtext":"Fully restored cathedral","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"lm3body"},"id":"notre-dame","bgImageSrc":"https://cdn.britannica.com/29/255529-050-63A22A3C/notre-dame-de-paris-cathedral-paris-france.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Reopened 2024","icon":{"type":"element","typeName":"Icon","props":{"name":"hammer","category":"tools"},"partial":false,"hasDynamicProps":false,"statementId":"lm3tagIcon"},"size":"sm","variant":"warning"},"partial":false,"hasDynamicProps":false,"statementId":"lm3tag"},"bgImageAlt":"Notre-Dame cathedral facade"},"partial":false,"hasDynamicProps":false,"statementId":"lm3"},{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Palace of Versailles","subtext":"Royal gardens & grand halls","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"lm4body"},"id":"versailles","bgImageSrc":"https://cdn.britannica.com/76/130076-050-31128A3D/Latona-Fountain-Balthazard-Marsy-Gaspard-Andre-Le.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Day Trip","icon":{"type":"element","typeName":"Icon","props":{"name":"train-front","category":"travel"},"partial":false,"hasDynamicProps":false,"statementId":"lm4tagIcon"},"size":"sm","variant":"neutral"},"partial":false,"hasDynamicProps":false,"statementId":"lm4tag"},"bgImageAlt":"Latona Fountain at Versailles"},"partial":false,"hasDynamicProps":false,"statementId":"lm4"}],"layout":"carousel","responsive":true,"action":{"type":"continue_conversation","context":"Tell me more about this Paris landmark"}},"partial":false,"hasDynamicProps":false,"statementId":"landmarksCarousel"}]},"partial":false,"hasDynamicProps":false,"statementId":"secLandmarks"},{"type":"element","typeName":"SectionItem","props":{"value":"food","trigger":"Food & Drink","content":[{"type":"element","typeName":"ContextCardBlock","props":{"items":[{"type":"element","typeName":"ContextCardItem","props":{"id":"bistros","title":"Bistros","body":"Classic French fare — try Les Arlots for confit beef cheeks.","bgColor":"gray","bgImageSrc":"https://everydayparisian.com/wp-content/uploads/2023/01/IMG_0221-768x1024.webp"},"partial":false,"hasDynamicProps":false,"statementId":"f1"},{"type":"element","typeName":"ContextCardItem","props":{"id":"fine-dining","title":"Fine Dining","body":"Septime leads a new wave of creative Michelin-starred cuisine.","bgColor":"gray","bgImageSrc":"https://production-data.worldofmouth.app/images/72b2e6db-97ea-49c4-aa5d-3b7c8121c812.jpg"},"partial":false,"hasDynamicProps":false,"statementId":"f2"},{"type":"element","typeName":"ContextCardItem","props":{"id":"bakeries","title":"Bakeries","body":"Shinya Pain for sourdough. Grand Prix-winning baguettes citywide.","bgColor":"gray","bgImageSrc":"https://www.davidlebovitz.com/wp-content/uploads/2018/09/Le-petit-grain-paris-bakery-boulangerie-patisserie-pastry-shop-8-640x895.jpg"},"partial":false,"hasDynamicProps":false,"statementId":"f3"}],"layout":"grid","responsive":true,"action":{"type":"continue_conversation","context":"Tell me more about eating and drinking in Paris"}},"partial":false,"hasDynamicProps":false,"statementId":"foodCards"}]},"partial":false,"hasDynamicProps":false,"statementId":"secFood"},{"type":"element","typeName":"SectionItem","props":{"value":"when","trigger":"Best Time to Visit","content":[{"type":"element","typeName":"Tabs","props":{"items":[{"type":"element","typeName":"TabItem","props":{"value":"spring","trigger":"Spring","content":[{"type":"element","typeName":"ListBlock","props":{"items":[{"type":"element","typeName":"ListItem","props":{"title":"Cherry blossoms at Parc de Sceaux","subtitle":"March – May","image":null,"actionLabel":null,"action":{"type":"continue_conversation","context":"Tell me about cherry blossom season in Paris"}},"partial":false,"hasDynamicProps":false,"statementId":"sp1"},{"type":"element","typeName":"ListItem","props":{"title":"Picnics along the Seine & in city parks","subtitle":"Warm, lively atmosphere","image":null,"actionLabel":null,"action":{"type":"continue_conversation","context":"Where are the best picnic spots in Paris?"}},"partial":false,"hasDynamicProps":false,"statementId":"sp2"},{"type":"element","typeName":"ListItem","props":{"title":"Fewer crowds than summer","subtitle":"Ideal for sightseeing","image":null,"actionLabel":null,"action":{"type":"continue_conversation","context":"How busy is Paris in spring?"}},"partial":false,"hasDynamicProps":false,"statementId":"sp3"}],"variant":"number"},"partial":false,"hasDynamicProps":false,"statementId":"springList"}]},"partial":false,"hasDynamicProps":false,"statementId":"tabSpring"},{"type":"element","typeName":"TabItem","props":{"value":"summer","trigger":"Summer","content":[{"type":"element","typeName":"ListBlock","props":{"items":[{"type":"element","typeName":"ListItem","props":{"title":"Swim in the Seine — open July & August 2026","subtitle":"First time in 102 years","image":null,"actionLabel":null,"action":{"type":"continue_conversation","context":"Tell me about swimming in the Seine"}},"partial":false,"hasDynamicProps":false,"statementId":"su1"},{"type":"element","typeName":"ListItem","props":{"title":"Open-air cinema at Parc de la Villette","subtitle":"Free films under the stars","image":null,"actionLabel":null,"action":{"type":"continue_conversation","context":"Tell me about the open-air cinema at Parc de la Villette"}},"partial":false,"hasDynamicProps":false,"statementId":"su2"},{"type":"element","typeName":"ListItem","props":{"title":"Bastille Day celebrations on July 14","subtitle":"Fireworks at the Eiffel Tower","image":null,"actionLabel":null,"action":{"type":"continue_conversation","context":"What happens in Paris on Bastille Day?"}},"partial":false,"hasDynamicProps":false,"statementId":"su3"}],"variant":"number"},"partial":false,"hasDynamicProps":false,"statementId":"summerList"}]},"partial":false,"hasDynamicProps":false,"statementId":"tabSummer"}]},"partial":false,"hasDynamicProps":false,"statementId":"whenTabs"}]},"partial":false,"hasDynamicProps":false,"statementId":"secWhen"}],"isFoldable":false},"partial":false,"hasDynamicProps":false,"statementId":"sectionsBlock"},{"type":"element","typeName":"Buttons","props":{"buttons":[{"type":"element","typeName":"Button","props":{"label":"Plan a Trip to Paris","action":{"type":"continue_conversation","context":"Help me plan a trip to Paris"},"variant":"primary"},"partial":false,"hasDynamicProps":false,"statementId":"btn1"},{"type":"element","typeName":"Button","props":{"label":"Best Hotels in Paris","action":{"type":"continue_conversation","context":"Show me the best hotels in Paris"},"variant":"secondary"},"partial":false,"hasDynamicProps":false,"statementId":"btn2"},{"type":"element","typeName":"Button","props":{"label":"Build a Paris Itinerary","action":{"type":"continue_conversation","context":"Build a 5-day Paris itinerary for me"},"variant":"secondary"},"partial":false,"hasDynamicProps":false,"statementId":"btn3"}],"direction":"row"},"partial":false,"hasDynamicProps":false,"statementId":"actionBtns"}],"sources":[{"title":"Paris Travel Guide 2025","sourceName":"Travel and Tour World","url":"https://www.travelandtourworld.com/news/article/from-iconic-landmarks-to-hidden-gems-the-ultimate-paris-travel-guide-for-2025-you-need-to-read-now/"},{"title":"Best Things to Do in Paris 2026","sourceName":"Time Out Paris","url":"http://www.timeout.fr/paris/en/for-tourists"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"errors":[]}}, +{"name":"chat-example-2","schema":"chat","input":"Example 2 — Data and analytics response (quantitative asks lead with KPIs and charts):\n\nroot = Card([pageHeader, kpiBlock, sections, deepDiveHeader, compositeCards, actionButtons])\npageHeader = CardHeader(\"Analytics Dashboard\", \"April 2026 · Business Performance Overview\")\nkpiBlock = SnippetCardBlock([kpi1, kpi2, kpi3, kpi4], \"grid\", true)\nkpi1 = SnippetCardItem(\"total-revenue\", kpi1lhs, kpi1rhs)\nkpi1lhs = IconText(kpi1icon, \"neutral\", \"m\", \"Total Revenue\", \"Month to date\", false, \"horizontal\")\nkpi1icon = Icon(\"circle-dollar-sign\", \"finance\")\nkpi1rhs = BoldText(\"number\", \"$284,500\", \"+12.4%\", \"metric\")\nkpi2 = SnippetCardItem(\"active-users\", kpi2lhs, kpi2rhs)\nkpi2lhs = IconText(kpi2icon, \"neutral\", \"m\", \"Active Users\", \"Monthly active users\", false, \"horizontal\")\nkpi2icon = Icon(\"users\", \"people\")\nkpi2rhs = BoldText(\"number\", \"47,320\", \"+6.8%\", \"metric\")\nkpi3 = SnippetCardItem(\"conversion-rate\", kpi3lhs, kpi3rhs)\nkpi3lhs = IconText(kpi3icon, \"neutral\", \"m\", \"Conversion Rate\", \"Avg. this month\", false, \"horizontal\")\nkpi3icon = Icon(\"trending-up\", \"charts\")\nkpi3rhs = BoldText(\"number\", \"5.7%\", \"+0.9%\", \"metric\")\nkpi4 = SnippetCardItem(\"churn-rate\", kpi4lhs, kpi4rhs)\nkpi4lhs = IconText(kpi4icon, \"neutral\", \"m\", \"Churn Rate\", \"Monthly customer churn\", false, \"horizontal\")\nkpi4icon = Icon(\"user-minus\", \"people\")\nkpi4rhs = BoldText(\"number\", \"2.1%\", \"-0.4%\", \"metric\")\nsections = SectionBlock([secUsers, secChannels], false)\nsecUsers = SectionItem(\"users\", \"User Activity\", [userTabs])\nuserTabs = Tabs([tabGrowth, tabRetention])\ntabGrowth = TabItem(\"growth\", \"User Growth\", [growthChart])\ngrowthChart = LineChart(months, [dauSeries, mauSeries], \"natural\", \"Month\", \"Users (k)\")\nmonths = [\"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\", \"Jan\", \"Feb\", \"Mar\", \"Apr\"]\ndauSeries = Series(\"DAU\", [5.1, 5.4, 5.8, 6.2, 6.7, 7.1, 7.6, 8.0, 8.4, 8.9, 9.3, 9.8])\nmauSeries = Series(\"MAU\", [28.4, 30.1, 31.8, 33.5, 35.2, 37.1, 38.8, 40.4, 42.1, 43.9, 45.6, 47.3])\ntabRetention = TabItem(\"retention\", \"Cohort Retention\", [retentionChart])\nretentionChart = LineChart(cohortMonths, [cohortJan, cohortFeb, cohortMar], \"natural\", \"Month Since Signup\", \"Retention (%)\")\ncohortMonths = [\"Month 0\", \"Month 1\", \"Month 2\", \"Month 3\", \"Month 4\", \"Month 5\", \"Month 6\"]\ncohortJan = Series(\"Jan Cohort\", [100, 80, 70, 62, 56, 51, 47])\ncohortFeb = Series(\"Feb Cohort\", [100, 83, 73, 65, 59, 54, 50])\ncohortMar = Series(\"Mar Cohort\", [100, 86, 76, 68, 62, 0, 0])\nsecChannels = SectionItem(\"channels\", \"Acquisition Channels\", [channelTabs])\nchannelTabs = Tabs([tabChannelBar, tabChannelTable])\ntabChannelBar = TabItem(\"channel-bar\", \"Revenue by Channel\", [channelBarChart])\nchannelBarChart = HorizontalBarChart(channels, [channelSeries], \"grouped\", \"Revenue ($k)\", \"Channel\")\nchannels = [\"Organic Search\", \"Paid Ads\", \"Referral Program\", \"Partner Network\", \"Direct / Brand\", \"Email Campaigns\"]\nchannelSeries = Series(\"Revenue ($k)\", [82.4, 68.1, 54.6, 38.2, 26.8, 14.4])\ntabChannelTable = TabItem(\"channel-table\", \"Full Breakdown\", [channelTable])\nchannelTable = Table([colChannel, colLeads, colConv, colCac, colRevenue, colStatus])\ncolChannel = Col(\"Channel\", channels, \"string\")\ncolLeads = Col(\"Leads\", [4210, 3180, 2050, 1120, 1290, 820], \"number\")\ncolConv = Col(\"Conv. Rate\", [\"6.4%\", \"5.1%\", \"9.4%\", \"11.8%\", \"5.8%\", \"8.1%\"], \"string\")\ncolCac = Col(\"CAC ($)\", [188, 395, 132, 109, 218, 154], \"number\")\ncolRevenue = Col(\"Revenue ($k)\", [82.4, 68.1, 54.6, 38.2, 26.8, 14.4], \"number\")\ncolStatus = Col(\"Status\", [\"Scaling\", \"Optimizing\", \"Scaling\", \"Growing\", \"Stable\", \"Stable\"], \"string\")\ndeepDiveHeader = InlineHeader(\"Support Health & Marketing ROI\", \"Click a card to explore deeper insights\")\ncompositeCards = CompositeCardBlock([cardSupport, cardMarketing], \"grid\", true, { type: \"continue_conversation\", context: \"Explore this deep-dive area in more detail\" })\ncardSupport = CompositeCardItem(\"support-health\", supportHeader, [supportMetric, supportChart, supportList])\nsupportHeader = IconText(supportIcon, \"neutral\", \"m\", \"Support Health\", \"Ticket volume & resolution trends\", false, \"horizontal\")\nsupportIcon = Icon(\"headphones\", \"communication\")\nsupportMetric = MetricIndicatorInline(\"94.2% CSAT\", \"Customer Satisfaction Score\", { direction: \"up\", value: 2.1 })\nsupportChart = BarChart([\"Jan\", \"Feb\", \"Mar\", \"Apr\"], [openSeries, resolvedSeries], \"grouped\", \"Month\", \"Tickets\")\nopenSeries = Series(\"Opened\", [341, 327, 308, 289])\nresolvedSeries = Series(\"Resolved\", [319, 318, 301, 283])\nsupportList = EntityList([sup1, sup2, sup3], \"small\")\nsup1 = { left: \"Avg. Resolution Time\", right: \"4h 12m\", rightVariant: \"text\" }\nsup2 = { left: \"First Contact Resolution\", right: \"78.4%\", rightVariant: \"number\" }\nsup3 = { left: \"Escalation Rate\", right: \"6.2%\", rightVariant: \"number\" }\ncardMarketing = CompositeCardItem(\"marketing-roi\", marketingHeader, [marketingMetric, marketingChart, marketingList])\nmarketingHeader = IconText(marketingIcon, \"neutral\", \"m\", \"Marketing ROI\", \"Spend vs. pipeline generated\", false, \"horizontal\")\nmarketingIcon = Icon(\"megaphone\", \"communication\")\nmarketingMetric = MetricIndicatorInline(\"3.8x Blended ROI\", \"Return on Ad Spend\", { direction: \"up\", value: 0.4 })\nmarketingChart = AreaChart([\"Jan\", \"Feb\", \"Mar\", \"Apr\"], [spendSeries, pipelineSeries], \"natural\", \"Month\", \"Value ($k)\")\nspendSeries = Series(\"Ad Spend\", [44.0, 46.8, 49.2, 51.6])\npipelineSeries = Series(\"Pipeline Generated\", [158.2, 171.5, 184.9, 196.1])\nmarketingList = EntityList([mkt1, mkt2, mkt3], \"small\")\nmkt1 = { left: \"Total Ad Spend (Apr)\", right: \"$51,600\", rightVariant: \"number\" }\nmkt2 = { left: \"Cost per Lead\", right: \"$48.20\", rightVariant: \"number\" }\nmkt3 = { left: \"Top Channel\", right: \"Organic Search\", rightVariant: \"text\" }\nactionButtons = Buttons([btnChannels, btnForecast], \"row\")\nbtnChannels = Button(\"Break Down by Channel\", { type: \"continue_conversation\", context: \"Break down this month's revenue by acquisition channel\" }, \"primary\")\nbtnForecast = Button(\"Forecast Next Quarter\", { type: \"continue_conversation\", context: \"Forecast revenue and active users for next quarter\" }, \"secondary\")","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Analytics Dashboard","subtitle":"April 2026 · Business Performance Overview"},"partial":false,"hasDynamicProps":false,"statementId":"pageHeader"},{"type":"element","typeName":"SnippetCardBlock","props":{"items":[{"type":"element","typeName":"SnippetCardItem","props":{"id":"total-revenue","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"circle-dollar-sign","category":"finance"},"partial":false,"hasDynamicProps":false,"statementId":"kpi1icon"},"iconVariant":"neutral","iconSize":"m","title":"Total Revenue","subtitle":"Month to date","bold":false,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"kpi1lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"$284,500","subtext":"+12.4%","subtextVariant":"metric","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"kpi1rhs"}},"partial":false,"hasDynamicProps":false,"statementId":"kpi1"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"active-users","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"users","category":"people"},"partial":false,"hasDynamicProps":false,"statementId":"kpi2icon"},"iconVariant":"neutral","iconSize":"m","title":"Active Users","subtitle":"Monthly active users","bold":false,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"kpi2lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"47,320","subtext":"+6.8%","subtextVariant":"metric","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"kpi2rhs"}},"partial":false,"hasDynamicProps":false,"statementId":"kpi2"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"conversion-rate","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"trending-up","category":"charts"},"partial":false,"hasDynamicProps":false,"statementId":"kpi3icon"},"iconVariant":"neutral","iconSize":"m","title":"Conversion Rate","subtitle":"Avg. this month","bold":false,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"kpi3lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"5.7%","subtext":"+0.9%","subtextVariant":"metric","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"kpi3rhs"}},"partial":false,"hasDynamicProps":false,"statementId":"kpi3"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"churn-rate","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"user-minus","category":"people"},"partial":false,"hasDynamicProps":false,"statementId":"kpi4icon"},"iconVariant":"neutral","iconSize":"m","title":"Churn Rate","subtitle":"Monthly customer churn","bold":false,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"kpi4lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"2.1%","subtext":"-0.4%","subtextVariant":"metric","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"kpi4rhs"}},"partial":false,"hasDynamicProps":false,"statementId":"kpi4"}],"layout":"grid","responsive":true},"partial":false,"hasDynamicProps":false,"statementId":"kpiBlock"},{"type":"element","typeName":"SectionBlock","props":{"sections":[{"type":"element","typeName":"SectionItem","props":{"value":"users","trigger":"User Activity","content":[{"type":"element","typeName":"Tabs","props":{"items":[{"type":"element","typeName":"TabItem","props":{"value":"growth","trigger":"User Growth","content":[{"type":"element","typeName":"LineChart","props":{"labels":["May","Jun","Jul","Aug","Sep","Oct","Nov","Dec","Jan","Feb","Mar","Apr"],"series":[{"type":"element","typeName":"Series","props":{"category":"DAU","values":[5.1,5.4,5.8,6.2,6.7,7.1,7.6,8,8.4,8.9,9.3,9.8]},"partial":false,"hasDynamicProps":false,"statementId":"dauSeries"},{"type":"element","typeName":"Series","props":{"category":"MAU","values":[28.4,30.1,31.8,33.5,35.2,37.1,38.8,40.4,42.1,43.9,45.6,47.3]},"partial":false,"hasDynamicProps":false,"statementId":"mauSeries"}],"variant":"natural","xLabel":"Month","yLabel":"Users (k)"},"partial":false,"hasDynamicProps":false,"statementId":"growthChart"}]},"partial":false,"hasDynamicProps":false,"statementId":"tabGrowth"},{"type":"element","typeName":"TabItem","props":{"value":"retention","trigger":"Cohort Retention","content":[{"type":"element","typeName":"LineChart","props":{"labels":["Month 0","Month 1","Month 2","Month 3","Month 4","Month 5","Month 6"],"series":[{"type":"element","typeName":"Series","props":{"category":"Jan Cohort","values":[100,80,70,62,56,51,47]},"partial":false,"hasDynamicProps":false,"statementId":"cohortJan"},{"type":"element","typeName":"Series","props":{"category":"Feb Cohort","values":[100,83,73,65,59,54,50]},"partial":false,"hasDynamicProps":false,"statementId":"cohortFeb"},{"type":"element","typeName":"Series","props":{"category":"Mar Cohort","values":[100,86,76,68,62,0,0]},"partial":false,"hasDynamicProps":false,"statementId":"cohortMar"}],"variant":"natural","xLabel":"Month Since Signup","yLabel":"Retention (%)"},"partial":false,"hasDynamicProps":false,"statementId":"retentionChart"}]},"partial":false,"hasDynamicProps":false,"statementId":"tabRetention"}]},"partial":false,"hasDynamicProps":false,"statementId":"userTabs"}]},"partial":false,"hasDynamicProps":false,"statementId":"secUsers"},{"type":"element","typeName":"SectionItem","props":{"value":"channels","trigger":"Acquisition Channels","content":[{"type":"element","typeName":"Tabs","props":{"items":[{"type":"element","typeName":"TabItem","props":{"value":"channel-bar","trigger":"Revenue by Channel","content":[{"type":"element","typeName":"HorizontalBarChart","props":{"labels":["Organic Search","Paid Ads","Referral Program","Partner Network","Direct / Brand","Email Campaigns"],"series":[{"type":"element","typeName":"Series","props":{"category":"Revenue ($k)","values":[82.4,68.1,54.6,38.2,26.8,14.4]},"partial":false,"hasDynamicProps":false,"statementId":"channelSeries"}],"variant":"grouped","xLabel":"Revenue ($k)","yLabel":"Channel"},"partial":false,"hasDynamicProps":false,"statementId":"channelBarChart"}]},"partial":false,"hasDynamicProps":false,"statementId":"tabChannelBar"},{"type":"element","typeName":"TabItem","props":{"value":"channel-table","trigger":"Full Breakdown","content":[{"type":"element","typeName":"Table","props":{"columns":[{"type":"element","typeName":"Col","props":{"label":"Channel","data":["Organic Search","Paid Ads","Referral Program","Partner Network","Direct / Brand","Email Campaigns"],"type":"string"},"partial":false,"hasDynamicProps":false,"statementId":"colChannel"},{"type":"element","typeName":"Col","props":{"label":"Leads","data":[4210,3180,2050,1120,1290,820],"type":"number"},"partial":false,"hasDynamicProps":false,"statementId":"colLeads"},{"type":"element","typeName":"Col","props":{"label":"Conv. Rate","data":["6.4%","5.1%","9.4%","11.8%","5.8%","8.1%"],"type":"string"},"partial":false,"hasDynamicProps":false,"statementId":"colConv"},{"type":"element","typeName":"Col","props":{"label":"CAC ($)","data":[188,395,132,109,218,154],"type":"number"},"partial":false,"hasDynamicProps":false,"statementId":"colCac"},{"type":"element","typeName":"Col","props":{"label":"Revenue ($k)","data":[82.4,68.1,54.6,38.2,26.8,14.4],"type":"number"},"partial":false,"hasDynamicProps":false,"statementId":"colRevenue"},{"type":"element","typeName":"Col","props":{"label":"Status","data":["Scaling","Optimizing","Scaling","Growing","Stable","Stable"],"type":"string"},"partial":false,"hasDynamicProps":false,"statementId":"colStatus"}]},"partial":false,"hasDynamicProps":false,"statementId":"channelTable"}]},"partial":false,"hasDynamicProps":false,"statementId":"tabChannelTable"}]},"partial":false,"hasDynamicProps":false,"statementId":"channelTabs"}]},"partial":false,"hasDynamicProps":false,"statementId":"secChannels"}],"isFoldable":false},"partial":false,"hasDynamicProps":false,"statementId":"sections"},{"type":"element","typeName":"InlineHeader","props":{"heading":"Support Health & Marketing ROI","description":"Click a card to explore deeper insights"},"partial":false,"hasDynamicProps":false,"statementId":"deepDiveHeader"},{"type":"element","typeName":"CompositeCardBlock","props":{"items":[{"type":"element","typeName":"CompositeCardItem","props":{"id":"support-health","header":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"headphones","category":"communication"},"partial":false,"hasDynamicProps":false,"statementId":"supportIcon"},"iconVariant":"neutral","iconSize":"m","title":"Support Health","subtitle":"Ticket volume & resolution trends","bold":false,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"supportHeader"},"body":[{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"94.2% CSAT","subtext":"Customer Satisfaction Score","trend":{"direction":"up","value":2.1}},"partial":false,"hasDynamicProps":false,"statementId":"supportMetric"},{"type":"element","typeName":"BarChart","props":{"labels":["Jan","Feb","Mar","Apr"],"series":[{"type":"element","typeName":"Series","props":{"category":"Opened","values":[341,327,308,289]},"partial":false,"hasDynamicProps":false,"statementId":"openSeries"},{"type":"element","typeName":"Series","props":{"category":"Resolved","values":[319,318,301,283]},"partial":false,"hasDynamicProps":false,"statementId":"resolvedSeries"}],"variant":"grouped","xLabel":"Month","yLabel":"Tickets"},"partial":false,"hasDynamicProps":false,"statementId":"supportChart"},{"type":"element","typeName":"EntityList","props":{"rows":[{"left":"Avg. Resolution Time","right":"4h 12m","rightVariant":"text"},{"left":"First Contact Resolution","right":"78.4%","rightVariant":"number"},{"left":"Escalation Rate","right":"6.2%","rightVariant":"number"}],"size":"small"},"partial":false,"hasDynamicProps":false,"statementId":"supportList"}]},"partial":false,"hasDynamicProps":false,"statementId":"cardSupport"},{"type":"element","typeName":"CompositeCardItem","props":{"id":"marketing-roi","header":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"megaphone","category":"communication"},"partial":false,"hasDynamicProps":false,"statementId":"marketingIcon"},"iconVariant":"neutral","iconSize":"m","title":"Marketing ROI","subtitle":"Spend vs. pipeline generated","bold":false,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"marketingHeader"},"body":[{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"3.8x Blended ROI","subtext":"Return on Ad Spend","trend":{"direction":"up","value":0.4}},"partial":false,"hasDynamicProps":false,"statementId":"marketingMetric"},{"type":"element","typeName":"AreaChart","props":{"labels":["Jan","Feb","Mar","Apr"],"series":[{"type":"element","typeName":"Series","props":{"category":"Ad Spend","values":[44,46.8,49.2,51.6]},"partial":false,"hasDynamicProps":false,"statementId":"spendSeries"},{"type":"element","typeName":"Series","props":{"category":"Pipeline Generated","values":[158.2,171.5,184.9,196.1]},"partial":false,"hasDynamicProps":false,"statementId":"pipelineSeries"}],"variant":"natural","xLabel":"Month","yLabel":"Value ($k)"},"partial":false,"hasDynamicProps":false,"statementId":"marketingChart"},{"type":"element","typeName":"EntityList","props":{"rows":[{"left":"Total Ad Spend (Apr)","right":"$51,600","rightVariant":"number"},{"left":"Cost per Lead","right":"$48.20","rightVariant":"number"},{"left":"Top Channel","right":"Organic Search","rightVariant":"text"}],"size":"small"},"partial":false,"hasDynamicProps":false,"statementId":"marketingList"}]},"partial":false,"hasDynamicProps":false,"statementId":"cardMarketing"}],"layout":"grid","responsive":true,"action":{"type":"continue_conversation","context":"Explore this deep-dive area in more detail"}},"partial":false,"hasDynamicProps":false,"statementId":"compositeCards"},{"type":"element","typeName":"Buttons","props":{"buttons":[{"type":"element","typeName":"Button","props":{"label":"Break Down by Channel","action":{"type":"continue_conversation","context":"Break down this month's revenue by acquisition channel"},"variant":"primary"},"partial":false,"hasDynamicProps":false,"statementId":"btnChannels"},{"type":"element","typeName":"Button","props":{"label":"Forecast Next Quarter","action":{"type":"continue_conversation","context":"Forecast revenue and active users for next quarter"},"variant":"secondary"},"partial":false,"hasDynamicProps":false,"statementId":"btnForecast"}],"direction":"row"},"partial":false,"hasDynamicProps":false,"statementId":"actionButtons"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"errors":[]}}, +{"name":"chat-example-3","schema":"chat","input":"Example 3 — Form response (help-me-choose/plan asks: inspiration cards FIRST, then a compact form). The inspiration-card images below came from an image tool call — include such cards ONLY when you have real URLs; with no real URLs, start directly with the form:\n\nroot = Card([header, intro, inspoCards, form])\nheader = CardHeader(\"Plan Your Trip\", \"A few ideas to spark inspiration — then tell me your preferences\")\nintro = TextContent(\"Tap a style to jump straight in, or fill the quick form below for a tailored plan.\")\ninspoCards = VisualCardBlock([insp1, insp2], \"grid\", true, { type: \"continue_conversation\", context: \"Plan me a trip in this style with destinations, itinerary, and costs\" })\ninsp1 = VisualCardItem(insp1body, \"inspo-beach\", \"https://www.travelandleisure.com/thmb/cuAB7XMQ6s_Gji-no956KNopT7M=/1500x0/filters:no_upscale():max_bytes(150000):strip_icc()/TAL-maldives-TROPVACAY0325-1b5047d5931d4850b47d082966f9f563.jpg\", insp1tag, \"Maldives overwater villas\")\ninsp1body = BoldText(\"text\", \"Tropical Beach Escape\", \"Overwater villas & snorkeling\")\ninsp1tag = Tag(\"Relaxation\", insp1icon, \"sm\", \"info\")\ninsp1icon = Icon(\"umbrella\", \"travel\")\ninsp2 = VisualCardItem(insp2body, \"inspo-mountain\", \"https://www.salkantaytrekmachu.com/img/adventure-travel-peru-032.jpg\", insp2tag, \"Trekking in the Peruvian Andes\")\ninsp2body = BoldText(\"text\", \"Mountain Adventure\", \"Trekking & epic views\")\ninsp2tag = Tag(\"Active\", insp2icon, \"sm\", \"warning\")\ninsp2icon = Icon(\"mountain\", \"nature\")\nform = Form(\"trip-planner\", formButtons, [fcDestination, fcDates, fcTravellers, fcTripType, fcBudget, fcStay, fcNotes])\nformButtons = Buttons([btnSubmit, btnSkip], \"row\")\nbtnSubmit = Button(\"Plan My Trip\", { type: \"continue_conversation\", context: \"Help me plan a trip based on my form submission\" }, \"primary\")\nbtnSkip = Button(\"Just Surprise Me\", { type: \"continue_conversation\", context: \"Skip the form and surprise me with a destination and itinerary\" }, \"secondary\")\nfcDestination = FormControl(\"Destination\", destinationInput, \"Leave blank if you'd like suggestions\")\ndestinationInput = Input(\"destination\", \"e.g. Tokyo, Japan — or leave blank\", \"text\")\nfcDates = FormControl(\"Travel Dates\", datesPicker, \"Select your start and end dates\")\ndatesPicker = DatePicker(\"travel-dates\", \"range\", { required: true })\nfcTravellers = FormControl(\"Number of Travellers\", travellersSelect, \"How many people are travelling?\")\ntravellersSelect = Select(\"num-travellers\", [tr1, tr2, tr3, tr4], \"Select number of travellers\", { required: true })\ntr1 = SelectItem(\"1\", \"1 — Solo\")\ntr2 = SelectItem(\"2\", \"2 — Couple\")\ntr3 = SelectItem(\"3-4\", \"3–4 People\")\ntr4 = SelectItem(\"5+\", \"5+ People\")\nfcTripType = FormControl(\"Trip Type\", tripTypeCards, \"What kind of trip are you planning?\")\ntripTypeCards = OptionCards(\"trip-type\", \"single\", [oc1, oc2, oc3, oc4], { required: true })\noc1 = OptionCard(\"adventure\", \"Adventure\", \"Hiking, trekking & outdoor activities\", oc1icon)\noc1icon = Icon(\"mountain\", \"nature\")\noc2 = OptionCard(\"beach\", \"Beach & Relaxation\", \"Sun, sand & leisure\", oc2icon)\noc2icon = Icon(\"waves\", \"nature\")\noc3 = OptionCard(\"cultural\", \"Cultural & Historical\", \"Museums, heritage & local experiences\", oc3icon)\noc3icon = Icon(\"landmark\", \"buildings\")\noc4 = OptionCard(\"city\", \"City Break\", \"Urban exploration & nightlife\", oc4icon)\noc4icon = Icon(\"building-2\", \"buildings\")\nfcBudget = FormControl(\"Estimated Budget (per person)\", budgetSlider, \"Drag to set your per-person budget in USD\")\nbudgetSlider = Slider(\"budget\", \"discrete\", 500, 10000, 500, [2500], \"Budget (USD)\")\nfcStay = FormControl(\"Accommodation Preference\", stayChips, \"Select all that apply\")\nstayChips = Chips(\"accommodation\", \"multiple\", [ch1, ch2, ch3, ch4], {}, [\"hotel\"])\nch1 = ChipItem(\"hotel\", \"Hotel\", ch1icon)\nch1icon = Icon(\"building\", \"buildings\")\nch2 = ChipItem(\"resort\", \"Resort\", ch2icon)\nch2icon = Icon(\"umbrella\", \"travel\")\nch3 = ChipItem(\"airbnb\", \"Vacation Rental\", ch3icon)\nch3icon = Icon(\"home\", \"home\")\nch4 = ChipItem(\"camping\", \"Camping / Glamping\", ch4icon)\nch4icon = Icon(\"tent\", \"nature\")\nfcNotes = FormControl(\"Special Requests or Notes\", notesArea, \"Any dietary needs, accessibility requirements, or specific interests?\")\nnotesArea = TextArea(\"special-requests\", \"e.g. vegetarian meals, wheelchair access, honeymoon surprise...\", 4)","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Plan Your Trip","subtitle":"A few ideas to spark inspiration — then tell me your preferences"},"partial":false,"hasDynamicProps":false,"statementId":"header"},{"type":"element","typeName":"TextContent","props":{"text":"Tap a style to jump straight in, or fill the quick form below for a tailored plan."},"partial":false,"hasDynamicProps":false,"statementId":"intro"},{"type":"element","typeName":"VisualCardBlock","props":{"items":[{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Tropical Beach Escape","subtext":"Overwater villas & snorkeling","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"insp1body"},"id":"inspo-beach","bgImageSrc":"https://www.travelandleisure.com/thmb/cuAB7XMQ6s_Gji-no956KNopT7M=/1500x0/filters:no_upscale():max_bytes(150000):strip_icc()/TAL-maldives-TROPVACAY0325-1b5047d5931d4850b47d082966f9f563.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Relaxation","icon":{"type":"element","typeName":"Icon","props":{"name":"umbrella","category":"travel"},"partial":false,"hasDynamicProps":false,"statementId":"insp1icon"},"size":"sm","variant":"info"},"partial":false,"hasDynamicProps":false,"statementId":"insp1tag"},"bgImageAlt":"Maldives overwater villas"},"partial":false,"hasDynamicProps":false,"statementId":"insp1"},{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Mountain Adventure","subtext":"Trekking & epic views","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"insp2body"},"id":"inspo-mountain","bgImageSrc":"https://www.salkantaytrekmachu.com/img/adventure-travel-peru-032.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Active","icon":{"type":"element","typeName":"Icon","props":{"name":"mountain","category":"nature"},"partial":false,"hasDynamicProps":false,"statementId":"insp2icon"},"size":"sm","variant":"warning"},"partial":false,"hasDynamicProps":false,"statementId":"insp2tag"},"bgImageAlt":"Trekking in the Peruvian Andes"},"partial":false,"hasDynamicProps":false,"statementId":"insp2"}],"layout":"grid","responsive":true,"action":{"type":"continue_conversation","context":"Plan me a trip in this style with destinations, itinerary, and costs"}},"partial":false,"hasDynamicProps":false,"statementId":"inspoCards"},{"type":"element","typeName":"Form","props":{"name":"trip-planner","buttons":{"type":"element","typeName":"Buttons","props":{"buttons":[{"type":"element","typeName":"Button","props":{"label":"Plan My Trip","action":{"type":"continue_conversation","context":"Help me plan a trip based on my form submission"},"variant":"primary"},"partial":false,"hasDynamicProps":false,"statementId":"btnSubmit"},{"type":"element","typeName":"Button","props":{"label":"Just Surprise Me","action":{"type":"continue_conversation","context":"Skip the form and surprise me with a destination and itinerary"},"variant":"secondary"},"partial":false,"hasDynamicProps":false,"statementId":"btnSkip"}],"direction":"row"},"partial":false,"hasDynamicProps":false,"statementId":"formButtons"},"fields":[{"type":"element","typeName":"FormControl","props":{"label":"Destination","input":{"type":"element","typeName":"Input","props":{"name":"destination","placeholder":"e.g. Tokyo, Japan — or leave blank","type":"text"},"partial":false,"hasDynamicProps":false,"statementId":"destinationInput"},"hint":"Leave blank if you'd like suggestions"},"partial":false,"hasDynamicProps":false,"statementId":"fcDestination"},{"type":"element","typeName":"FormControl","props":{"label":"Travel Dates","input":{"type":"element","typeName":"DatePicker","props":{"name":"travel-dates","mode":"range","rules":{"required":true}},"partial":false,"hasDynamicProps":false,"statementId":"datesPicker"},"hint":"Select your start and end dates"},"partial":false,"hasDynamicProps":false,"statementId":"fcDates"},{"type":"element","typeName":"FormControl","props":{"label":"Number of Travellers","input":{"type":"element","typeName":"Select","props":{"name":"num-travellers","items":[{"type":"element","typeName":"SelectItem","props":{"value":"1","label":"1 — Solo"},"partial":false,"hasDynamicProps":false,"statementId":"tr1"},{"type":"element","typeName":"SelectItem","props":{"value":"2","label":"2 — Couple"},"partial":false,"hasDynamicProps":false,"statementId":"tr2"},{"type":"element","typeName":"SelectItem","props":{"value":"3-4","label":"3–4 People"},"partial":false,"hasDynamicProps":false,"statementId":"tr3"},{"type":"element","typeName":"SelectItem","props":{"value":"5+","label":"5+ People"},"partial":false,"hasDynamicProps":false,"statementId":"tr4"}],"placeholder":"Select number of travellers","rules":{"required":true}},"partial":false,"hasDynamicProps":false,"statementId":"travellersSelect"},"hint":"How many people are travelling?"},"partial":false,"hasDynamicProps":false,"statementId":"fcTravellers"},{"type":"element","typeName":"FormControl","props":{"label":"Trip Type","input":{"type":"element","typeName":"OptionCards","props":{"name":"trip-type","type":"single","items":[{"type":"element","typeName":"OptionCard","props":{"value":"adventure","title":"Adventure","subtitle":"Hiking, trekking & outdoor activities","topContent":{"type":"element","typeName":"Icon","props":{"name":"mountain","category":"nature"},"partial":false,"hasDynamicProps":false,"statementId":"oc1icon"}},"partial":false,"hasDynamicProps":false,"statementId":"oc1"},{"type":"element","typeName":"OptionCard","props":{"value":"beach","title":"Beach & Relaxation","subtitle":"Sun, sand & leisure","topContent":{"type":"element","typeName":"Icon","props":{"name":"waves","category":"nature"},"partial":false,"hasDynamicProps":false,"statementId":"oc2icon"}},"partial":false,"hasDynamicProps":false,"statementId":"oc2"},{"type":"element","typeName":"OptionCard","props":{"value":"cultural","title":"Cultural & Historical","subtitle":"Museums, heritage & local experiences","topContent":{"type":"element","typeName":"Icon","props":{"name":"landmark","category":"buildings"},"partial":false,"hasDynamicProps":false,"statementId":"oc3icon"}},"partial":false,"hasDynamicProps":false,"statementId":"oc3"},{"type":"element","typeName":"OptionCard","props":{"value":"city","title":"City Break","subtitle":"Urban exploration & nightlife","topContent":{"type":"element","typeName":"Icon","props":{"name":"building-2","category":"buildings"},"partial":false,"hasDynamicProps":false,"statementId":"oc4icon"}},"partial":false,"hasDynamicProps":false,"statementId":"oc4"}],"rules":{"required":true}},"partial":false,"hasDynamicProps":false,"statementId":"tripTypeCards"},"hint":"What kind of trip are you planning?"},"partial":false,"hasDynamicProps":false,"statementId":"fcTripType"},{"type":"element","typeName":"FormControl","props":{"label":"Estimated Budget (per person)","input":{"type":"element","typeName":"Slider","props":{"name":"budget","variant":"discrete","min":500,"max":10000,"step":500,"defaultValue":[2500],"label":"Budget (USD)"},"partial":false,"hasDynamicProps":false,"statementId":"budgetSlider"},"hint":"Drag to set your per-person budget in USD"},"partial":false,"hasDynamicProps":false,"statementId":"fcBudget"},{"type":"element","typeName":"FormControl","props":{"label":"Accommodation Preference","input":{"type":"element","typeName":"Chips","props":{"name":"accommodation","type":"multiple","items":[{"type":"element","typeName":"ChipItem","props":{"value":"hotel","label":"Hotel","icon":{"type":"element","typeName":"Icon","props":{"name":"building","category":"buildings"},"partial":false,"hasDynamicProps":false,"statementId":"ch1icon"}},"partial":false,"hasDynamicProps":false,"statementId":"ch1"},{"type":"element","typeName":"ChipItem","props":{"value":"resort","label":"Resort","icon":{"type":"element","typeName":"Icon","props":{"name":"umbrella","category":"travel"},"partial":false,"hasDynamicProps":false,"statementId":"ch2icon"}},"partial":false,"hasDynamicProps":false,"statementId":"ch2"},{"type":"element","typeName":"ChipItem","props":{"value":"airbnb","label":"Vacation Rental","icon":{"type":"element","typeName":"Icon","props":{"name":"home","category":"home"},"partial":false,"hasDynamicProps":false,"statementId":"ch3icon"}},"partial":false,"hasDynamicProps":false,"statementId":"ch3"},{"type":"element","typeName":"ChipItem","props":{"value":"camping","label":"Camping / Glamping","icon":{"type":"element","typeName":"Icon","props":{"name":"tent","category":"nature"},"partial":false,"hasDynamicProps":false,"statementId":"ch4icon"}},"partial":false,"hasDynamicProps":false,"statementId":"ch4"}],"rules":{},"defaultValue":["hotel"]},"partial":false,"hasDynamicProps":false,"statementId":"stayChips"},"hint":"Select all that apply"},"partial":false,"hasDynamicProps":false,"statementId":"fcStay"},{"type":"element","typeName":"FormControl","props":{"label":"Special Requests or Notes","input":{"type":"element","typeName":"TextArea","props":{"name":"special-requests","placeholder":"e.g. vegetarian meals, wheelchair access, honeymoon surprise...","rows":4},"partial":false,"hasDynamicProps":false,"statementId":"notesArea"},"hint":"Any dietary needs, accessibility requirements, or specific interests?"},"partial":false,"hasDynamicProps":false,"statementId":"fcNotes"}]},"partial":false,"hasDynamicProps":false,"statementId":"form"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"errors":[]}}, +{"name":"chat-example-4","schema":"chat","input":"Example 4 — Recommendations with images and links (same pattern for products, movies, restaurants, articles):\n\nroot = Card([header, intro, posters, linksHeader, trackCards], [src1, src2])\nheader = CardHeader(\"Retro-Energetic Indian Tracks\", \"New Hindi releases with 80s/90s dance DNA\")\nintro = TextContent(\"These recent tracks channel retro synths and 90s party percussion [1][2]. Tap a poster to explore a track, or use its button to watch the exact official video.\")\nposters = VisualCardBlock([v1, v2], \"grid\", true, { type: \"continue_conversation\", context: \"Tell me more about this track and its retro influences\" })\nv1 = VisualCardItem(v1body, \"tamma\", \"https://i.ytimg.com/vi/PQHeOb0Q9oo/hq720.jpg\", v1tag, \"Tamma Tamma video poster\")\nv1body = BoldText(\"text\", \"Tamma Tamma\", \"Dhurandhar · Ranveer Singh\")\nv1tag = Tag(\"80s Electro-Disco\", v1icon, \"sm\", \"success\")\nv1icon = Icon(\"disc-3\", \"multimedia\")\nv2 = VisualCardItem(v2body, \"sajan\", \"https://i.ytimg.com/vi/mLz-qBiRnl8/hq720.jpg\", v2tag, \"Sajan Re video poster\")\nv2body = BoldText(\"text\", \"Sajan Re\", \"Nora Fatehi · Badshah\")\nv2tag = Tag(\"Synthwave Electro\", v2icon, \"sm\", \"info\")\nv2icon = Icon(\"music\", \"multimedia\")\nlinksHeader = InlineHeader(\"Watch the Exact Videos\", \"Each button opens that track's official video\")\ntrackCards = CompositeCardBlock([t1, t2], \"grid\", true)\nt1 = CompositeCardItem(\"t-tamma\", t1h, [t1body, t1tags], { button: t1btn })\nt1h = IconText(t1icon, \"neutral\", \"m\", \"Tamma Tamma\", \"Bappi Lahiri classic, electro-funk rework\", true, \"horizontal\")\nt1icon = Icon(\"disc-3\", \"multimedia\")\nt1body = Text(\"text\", \"Heavy analog synths and brass over a fast disco groove.\", \"Retro Electro-Disco\")\nt1tags = TagBlock([\"2026 Film Hit\", \"Peak Party\"])\nt1btn = Button(\"Watch on YouTube\", { type: \"open_url\", url: \"https://www.youtube.com/watch?v=PQHeOb0Q9oo\" }, \"primary\")\nt2 = CompositeCardItem(\"t-sajan\", t2h, [t2body, t2tags], { button: t2btn })\nt2h = IconText(t2icon, \"neutral\", \"m\", \"Sajan Re\", \"Nora Fatehi · Badshah · Sanjoy\", true, \"horizontal\")\nt2icon = Icon(\"music\", \"multimedia\")\nt2body = Text(\"text\", \"Synthwave melody hooks over a modern club beat.\", \"Retro-Fusion Electro\")\nt2tags = TagBlock([\"Club Anthem\", \"Nora x Badshah\"])\nt2btn = Button(\"Watch on YouTube\", { type: \"open_url\", url: \"https://www.youtube.com/watch?v=COUSCnFazzc\" }, \"primary\")\nsrc1 = { title: \"Tamma Tamma (Full Video) - Dhurandhar\", sourceName: \"YouTube (T-Series)\", url: \"https://www.youtube.com/watch?v=PQHeOb0Q9oo\" }\nsrc2 = { title: \"Sajan Re (Official Music Video)\", sourceName: \"YouTube (T-Series)\", url: \"https://www.youtube.com/watch?v=COUSCnFazzc\" }","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Retro-Energetic Indian Tracks","subtitle":"New Hindi releases with 80s/90s dance DNA"},"partial":false,"hasDynamicProps":false,"statementId":"header"},{"type":"element","typeName":"TextContent","props":{"text":"These recent tracks channel retro synths and 90s party percussion [1][2]. Tap a poster to explore a track, or use its button to watch the exact official video."},"partial":false,"hasDynamicProps":false,"statementId":"intro"},{"type":"element","typeName":"VisualCardBlock","props":{"items":[{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Tamma Tamma","subtext":"Dhurandhar · Ranveer Singh","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"v1body"},"id":"tamma","bgImageSrc":"https://i.ytimg.com/vi/PQHeOb0Q9oo/hq720.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"80s Electro-Disco","icon":{"type":"element","typeName":"Icon","props":{"name":"disc-3","category":"multimedia"},"partial":false,"hasDynamicProps":false,"statementId":"v1icon"},"size":"sm","variant":"success"},"partial":false,"hasDynamicProps":false,"statementId":"v1tag"},"bgImageAlt":"Tamma Tamma video poster"},"partial":false,"hasDynamicProps":false,"statementId":"v1"},{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Sajan Re","subtext":"Nora Fatehi · Badshah","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"v2body"},"id":"sajan","bgImageSrc":"https://i.ytimg.com/vi/mLz-qBiRnl8/hq720.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Synthwave Electro","icon":{"type":"element","typeName":"Icon","props":{"name":"music","category":"multimedia"},"partial":false,"hasDynamicProps":false,"statementId":"v2icon"},"size":"sm","variant":"info"},"partial":false,"hasDynamicProps":false,"statementId":"v2tag"},"bgImageAlt":"Sajan Re video poster"},"partial":false,"hasDynamicProps":false,"statementId":"v2"}],"layout":"grid","responsive":true,"action":{"type":"continue_conversation","context":"Tell me more about this track and its retro influences"}},"partial":false,"hasDynamicProps":false,"statementId":"posters"},{"type":"element","typeName":"InlineHeader","props":{"heading":"Watch the Exact Videos","description":"Each button opens that track's official video"},"partial":false,"hasDynamicProps":false,"statementId":"linksHeader"},{"type":"element","typeName":"CompositeCardBlock","props":{"items":[{"type":"element","typeName":"CompositeCardItem","props":{"id":"t-tamma","header":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"disc-3","category":"multimedia"},"partial":false,"hasDynamicProps":false,"statementId":"t1icon"},"iconVariant":"neutral","iconSize":"m","title":"Tamma Tamma","subtitle":"Bappi Lahiri classic, electro-funk rework","bold":true,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"t1h"},"body":[{"type":"element","typeName":"Text","props":{"variant":"text","value":"Heavy analog synths and brass over a fast disco groove.","subtext":"Retro Electro-Disco","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"t1body"},{"type":"element","typeName":"TagBlock","props":{"tags":["2026 Film Hit","Peak Party"]},"partial":false,"hasDynamicProps":false,"statementId":"t1tags"}],"footer":{"button":{"type":"element","typeName":"Button","props":{"label":"Watch on YouTube","action":{"type":"open_url","url":"https://www.youtube.com/watch?v=PQHeOb0Q9oo"},"variant":"primary"},"partial":false,"hasDynamicProps":false,"statementId":"t1btn"}}},"partial":false,"hasDynamicProps":false,"statementId":"t1"},{"type":"element","typeName":"CompositeCardItem","props":{"id":"t-sajan","header":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"music","category":"multimedia"},"partial":false,"hasDynamicProps":false,"statementId":"t2icon"},"iconVariant":"neutral","iconSize":"m","title":"Sajan Re","subtitle":"Nora Fatehi · Badshah · Sanjoy","bold":true,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"t2h"},"body":[{"type":"element","typeName":"Text","props":{"variant":"text","value":"Synthwave melody hooks over a modern club beat.","subtext":"Retro-Fusion Electro","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"t2body"},{"type":"element","typeName":"TagBlock","props":{"tags":["Club Anthem","Nora x Badshah"]},"partial":false,"hasDynamicProps":false,"statementId":"t2tags"}],"footer":{"button":{"type":"element","typeName":"Button","props":{"label":"Watch on YouTube","action":{"type":"open_url","url":"https://www.youtube.com/watch?v=COUSCnFazzc"},"variant":"primary"},"partial":false,"hasDynamicProps":false,"statementId":"t2btn"}}},"partial":false,"hasDynamicProps":false,"statementId":"t2"}],"layout":"grid","responsive":true},"partial":false,"hasDynamicProps":false,"statementId":"trackCards"}],"sources":[{"title":"Tamma Tamma (Full Video) - Dhurandhar","sourceName":"YouTube (T-Series)","url":"https://www.youtube.com/watch?v=PQHeOb0Q9oo"},{"title":"Sajan Re (Official Music Video)","sourceName":"YouTube (T-Series)","url":"https://www.youtube.com/watch?v=COUSCnFazzc"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"errors":[]}}, +{"name":"chat-example-5","schema":"chat","input":"Example 5 — Table with follow-ups:\n\nroot = Card([title, tbl, followUps])\ntitle = TextContent(\"Top Languages\", \"large-heavy\")\ntbl = Table([Col(\"Language\", langs), Col(\"Users (M)\", users), Col(\"Year\", years)])\nlangs = [\"Python\", \"JavaScript\", \"Java\"]\nusers = [15.7, 14.2, 12.1]\nyears = [1991, 1995, 1995]\nfollowUps = FollowUpBlock([fu1, fu2])\nfu1 = FollowUpItem(\"Tell me more about Python\")\nfu2 = FollowUpItem(\"Show me a JavaScript comparison\")","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"Top Languages","size":"large-heavy"},"partial":false,"hasDynamicProps":false,"statementId":"title"},{"type":"element","typeName":"Table","props":{"columns":[{"type":"element","typeName":"Col","props":{"label":"Language","data":["Python","JavaScript","Java"]},"partial":false,"hasDynamicProps":false},{"type":"element","typeName":"Col","props":{"label":"Users (M)","data":[15.7,14.2,12.1]},"partial":false,"hasDynamicProps":false},{"type":"element","typeName":"Col","props":{"label":"Year","data":[1991,1995,1995]},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"tbl"},{"type":"element","typeName":"FollowUpBlock","props":{"items":[{"type":"element","typeName":"FollowUpItem","props":{"text":"Tell me more about Python"},"partial":false,"hasDynamicProps":false,"statementId":"fu1"},{"type":"element","typeName":"FollowUpItem","props":{"text":"Show me a JavaScript comparison"},"partial":false,"hasDynamicProps":false,"statementId":"fu2"}]},"partial":false,"hasDynamicProps":false,"statementId":"followUps"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"errors":[]}}, +{"name":"openui-example-1","schema":"openui","input":"Example 1 — Table (column-oriented):\n\nroot = Stack([title, tbl])\ntitle = TextContent(\"Top Languages\", \"large-heavy\")\ntbl = Table([Col(\"Language\", langs), Col(\"Users (M)\", users), Col(\"Year\", years)])\nlangs = [\"Python\", \"JavaScript\", \"Java\", \"TypeScript\", \"Go\"]\nusers = [15.7, 14.2, 12.1, 8.5, 5.2]\nyears = [1991, 1995, 1995, 2012, 2009]","expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"Top Languages","size":"large-heavy"},"partial":false,"hasDynamicProps":false,"statementId":"title"},{"type":"element","typeName":"Table","props":{"columns":[{"type":"element","typeName":"Col","props":{"label":"Language","data":["Python","JavaScript","Java","TypeScript","Go"]},"partial":false,"hasDynamicProps":false},{"type":"element","typeName":"Col","props":{"label":"Users (M)","data":[15.7,14.2,12.1,8.5,5.2]},"partial":false,"hasDynamicProps":false},{"type":"element","typeName":"Col","props":{"label":"Year","data":[1991,1995,1995,2012,2009]},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"tbl"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"errors":[]}}, +{"name":"openui-example-2","schema":"openui","input":"Example 2 — Bar chart:\n\nroot = Stack([title, chart])\ntitle = TextContent(\"Q4 Revenue\", \"large-heavy\")\nchart = BarChart(labels, [s1, s2], \"grouped\")\nlabels = [\"Oct\", \"Nov\", \"Dec\"]\ns1 = Series(\"Product A\", [120, 150, 180])\ns2 = Series(\"Product B\", [90, 110, 140])","expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"Q4 Revenue","size":"large-heavy"},"partial":false,"hasDynamicProps":false,"statementId":"title"},{"type":"element","typeName":"BarChart","props":{"labels":["Oct","Nov","Dec"],"series":[{"type":"element","typeName":"Series","props":{"category":"Product A","values":[120,150,180]},"partial":false,"hasDynamicProps":false,"statementId":"s1"},{"type":"element","typeName":"Series","props":{"category":"Product B","values":[90,110,140]},"partial":false,"hasDynamicProps":false,"statementId":"s2"}],"variant":"grouped"},"partial":false,"hasDynamicProps":false,"statementId":"chart"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"errors":[]}}, +{"name":"openui-example-3","schema":"openui","input":"Example 3 — Form with validation:\n\nroot = Stack([title, form])\ntitle = TextContent(\"Contact Us\", \"large-heavy\")\nform = Form(\"contact\", btns, [nameField, emailField, countryField, msgField])\nnameField = FormControl(\"Name\", Input(\"name\", \"Your name\", \"text\", { required: true, minLength: 2 }))\nemailField = FormControl(\"Email\", Input(\"email\", \"you@example.com\", \"email\", { required: true, email: true }))\ncountryField = FormControl(\"Country\", Select(\"country\", countryOpts, \"Select...\", { required: true }))\nmsgField = FormControl(\"Message\", TextArea(\"message\", \"Tell us more...\", 4, { required: true, minLength: 10 }))\ncountryOpts = [SelectItem(\"us\", \"United States\"), SelectItem(\"uk\", \"United Kingdom\"), SelectItem(\"de\", \"Germany\")]\nbtns = Buttons([Button(\"Submit\", Action([@ToAssistant(\"Submit\")]), \"primary\"), Button(\"Cancel\", Action([@ToAssistant(\"Cancel\")]), \"secondary\")])","expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"Contact Us","size":"large-heavy"},"partial":false,"hasDynamicProps":false,"statementId":"title"},{"type":"element","typeName":"Form","props":{"name":"contact","buttons":{"type":"element","typeName":"Buttons","props":{"buttons":[{"type":"element","typeName":"Button","props":{"label":"Submit","action":{"steps":[{"type":"continue_conversation","message":"Submit"}]},"variant":"primary"},"partial":false,"hasDynamicProps":true},{"type":"element","typeName":"Button","props":{"label":"Cancel","action":{"steps":[{"type":"continue_conversation","message":"Cancel"}]},"variant":"secondary"},"partial":false,"hasDynamicProps":true}]},"partial":false,"hasDynamicProps":true,"statementId":"btns"},"fields":[{"type":"element","typeName":"FormControl","props":{"label":"Name","input":{"type":"element","typeName":"Input","props":{"name":"name","placeholder":"Your name","type":"text","rules":{"required":true,"minLength":2}},"partial":false,"hasDynamicProps":false}},"partial":false,"hasDynamicProps":false,"statementId":"nameField"},{"type":"element","typeName":"FormControl","props":{"label":"Email","input":{"type":"element","typeName":"Input","props":{"name":"email","placeholder":"you@example.com","type":"email","rules":{"required":true,"email":true}},"partial":false,"hasDynamicProps":false}},"partial":false,"hasDynamicProps":false,"statementId":"emailField"},{"type":"element","typeName":"FormControl","props":{"label":"Country","input":{"type":"element","typeName":"Select","props":{"name":"country","items":[{"type":"element","typeName":"SelectItem","props":{"value":"us","label":"United States"},"partial":false,"hasDynamicProps":false},{"type":"element","typeName":"SelectItem","props":{"value":"uk","label":"United Kingdom"},"partial":false,"hasDynamicProps":false},{"type":"element","typeName":"SelectItem","props":{"value":"de","label":"Germany"},"partial":false,"hasDynamicProps":false}],"placeholder":"Select...","rules":{"required":true}},"partial":false,"hasDynamicProps":false}},"partial":false,"hasDynamicProps":false,"statementId":"countryField"},{"type":"element","typeName":"FormControl","props":{"label":"Message","input":{"type":"element","typeName":"TextArea","props":{"name":"message","placeholder":"Tell us more...","rows":4,"rules":{"required":true,"minLength":10}},"partial":false,"hasDynamicProps":false}},"partial":false,"hasDynamicProps":false,"statementId":"msgField"}]},"partial":false,"hasDynamicProps":true,"statementId":"form"}]},"partial":false,"hasDynamicProps":true,"statementId":"root"},"errors":[]}}, +{"name":"openui-example-4","schema":"openui","input":"Example 4 — Tabs with mixed content:\n\nroot = Stack([title, tabs])\ntitle = TextContent(\"React vs Vue\", \"large-heavy\")\ntabs = Tabs([tabReact, tabVue])\ntabReact = TabItem(\"react\", \"React\", reactContent)\ntabVue = TabItem(\"vue\", \"Vue\", vueContent)\nreactContent = [TextContent(\"React is a library by Meta for building UIs.\"), Callout(\"info\", \"Note\", \"React uses JSX syntax.\")]\nvueContent = [TextContent(\"Vue is a progressive framework by Evan You.\"), Callout(\"success\", \"Tip\", \"Vue has a gentle learning curve.\")]","expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"React vs Vue","size":"large-heavy"},"partial":false,"hasDynamicProps":false,"statementId":"title"},{"type":"element","typeName":"Tabs","props":{"items":[{"type":"element","typeName":"TabItem","props":{"value":"react","trigger":"React","content":[{"type":"element","typeName":"TextContent","props":{"text":"React is a library by Meta for building UIs."},"partial":false,"hasDynamicProps":false},{"type":"element","typeName":"Callout","props":{"variant":"info","title":"Note","description":"React uses JSX syntax."},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"tabReact"},{"type":"element","typeName":"TabItem","props":{"value":"vue","trigger":"Vue","content":[{"type":"element","typeName":"TextContent","props":{"text":"Vue is a progressive framework by Evan You."},"partial":false,"hasDynamicProps":false},{"type":"element","typeName":"Callout","props":{"variant":"success","title":"Tip","description":"Vue has a gentle learning curve."},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"tabVue"}]},"partial":false,"hasDynamicProps":false,"statementId":"tabs"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"errors":[]}}, +{"name":"openui-example-5","schema":"openui","input":"Example 5 — KPI and product cards:\n\nroot = Stack([kpiHeader, kpiBlock, productsHeader, productCards])\nkpiHeader = InlineHeader(\"This Month\", \"Key account metrics\")\nkpiBlock = SnippetCardBlock([kpi1, kpi2, kpi3])\nkpi1 = SnippetCardItem(\"revenue\", kpi1lhs, kpi1rhs)\nkpi1lhs = IconText(kpi1icon, \"neutral\", \"m\", \"Revenue\", \"Month to date\", false, \"horizontal\")\nkpi1icon = Icon(\"circle-dollar-sign\", \"finance\")\nkpi1rhs = BoldText(\"number\", \"$48,200\", \"+8.1%\", \"metric\")\nkpi2 = SnippetCardItem(\"active-users\", kpi2lhs, kpi2rhs)\nkpi2lhs = IconText(kpi2icon, \"neutral\", \"m\", \"Active Users\", \"This month\", false, \"horizontal\")\nkpi2icon = Icon(\"users\", \"people\")\nkpi2rhs = BoldText(\"number\", \"1,204\", \"+3.4%\", \"metric\")\nkpi3 = SnippetCardItem(\"churn\", kpi3lhs, kpi3rhs)\nkpi3lhs = IconText(kpi3icon, \"neutral\", \"m\", \"Churn\", \"This month\", false, \"horizontal\")\nkpi3icon = Icon(\"user-minus\", \"people\")\nkpi3rhs = BoldText(\"number\", \"1.8%\", \"-0.3%\", \"metric\")\nproductsHeader = InlineHeader(\"Top Products\", \"By units sold\")\nproductCards = CompositeCardBlock([p1, p2])\np1 = CompositeCardItem(\"widget-pro\", p1header, [p1body], p1footer)\np1header = IconText(p1icon, \"neutral\", \"m\", \"Widget Pro\", \"Best seller\", true, \"horizontal\")\np1icon = Icon(\"package\", \"shopping\")\np1body = MetricIndicatorInline(\"2,410 units\", \"Sold this month\", { direction: \"up\", value: 12 })\np1footer = { price: BoldText(\"text\", \"$29.00\"), button: p1btn }\np1btn = Button(\"View Details\", Action([@ToAssistant(\"Show details for Widget Pro\")]), \"secondary\")\np2 = CompositeCardItem(\"widget-mini\", p2header, [p2body], p2footer)\np2header = IconText(p2icon, \"neutral\", \"m\", \"Widget Mini\", \"New arrival\", true, \"horizontal\")\np2icon = Icon(\"box\", \"shopping\")\np2body = MetricIndicatorInline(\"980 units\", \"Sold this month\", { direction: \"up\", value: 4 })\np2footer = { price: BoldText(\"text\", \"$14.00\"), button: p2btn }\np2btn = Button(\"View Details\", Action([@ToAssistant(\"Show details for Widget Mini\")]), \"secondary\")","expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"InlineHeader","props":{"heading":"This Month","description":"Key account metrics"},"partial":false,"hasDynamicProps":false,"statementId":"kpiHeader"},{"type":"element","typeName":"SnippetCardBlock","props":{"items":[{"type":"element","typeName":"SnippetCardItem","props":{"id":"revenue","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"circle-dollar-sign","category":"finance"},"partial":false,"hasDynamicProps":false,"statementId":"kpi1icon"},"iconVariant":"neutral","iconSize":"m","title":"Revenue","subtitle":"Month to date","bold":false,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"kpi1lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"$48,200","subtext":"+8.1%","subtextVariant":"metric","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"kpi1rhs"}},"partial":false,"hasDynamicProps":false,"statementId":"kpi1"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"active-users","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"users","category":"people"},"partial":false,"hasDynamicProps":false,"statementId":"kpi2icon"},"iconVariant":"neutral","iconSize":"m","title":"Active Users","subtitle":"This month","bold":false,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"kpi2lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"1,204","subtext":"+3.4%","subtextVariant":"metric","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"kpi2rhs"}},"partial":false,"hasDynamicProps":false,"statementId":"kpi2"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"churn","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"user-minus","category":"people"},"partial":false,"hasDynamicProps":false,"statementId":"kpi3icon"},"iconVariant":"neutral","iconSize":"m","title":"Churn","subtitle":"This month","bold":false,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"kpi3lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"1.8%","subtext":"-0.3%","subtextVariant":"metric","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"kpi3rhs"}},"partial":false,"hasDynamicProps":false,"statementId":"kpi3"}],"layout":"grid","responsive":true},"partial":false,"hasDynamicProps":false,"statementId":"kpiBlock"},{"type":"element","typeName":"InlineHeader","props":{"heading":"Top Products","description":"By units sold"},"partial":false,"hasDynamicProps":false,"statementId":"productsHeader"},{"type":"element","typeName":"CompositeCardBlock","props":{"items":[{"type":"element","typeName":"CompositeCardItem","props":{"id":"widget-pro","header":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"package","category":"shopping"},"partial":false,"hasDynamicProps":false,"statementId":"p1icon"},"iconVariant":"neutral","iconSize":"m","title":"Widget Pro","subtitle":"Best seller","bold":true,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"p1header"},"body":[{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"2,410 units","subtext":"Sold this month","trend":{"direction":"up","value":12}},"partial":false,"hasDynamicProps":false,"statementId":"p1body"}],"footer":{"price":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"$29.00","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false},"button":{"type":"element","typeName":"Button","props":{"label":"View Details","action":{"steps":[{"type":"continue_conversation","message":"Show details for Widget Pro"}]},"variant":"secondary"},"partial":false,"hasDynamicProps":true,"statementId":"p1btn"}}},"partial":false,"hasDynamicProps":true,"statementId":"p1"},{"type":"element","typeName":"CompositeCardItem","props":{"id":"widget-mini","header":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"box","category":"shopping"},"partial":false,"hasDynamicProps":false,"statementId":"p2icon"},"iconVariant":"neutral","iconSize":"m","title":"Widget Mini","subtitle":"New arrival","bold":true,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"p2header"},"body":[{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"980 units","subtext":"Sold this month","trend":{"direction":"up","value":4}},"partial":false,"hasDynamicProps":false,"statementId":"p2body"}],"footer":{"price":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"$14.00","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false},"button":{"type":"element","typeName":"Button","props":{"label":"View Details","action":{"steps":[{"type":"continue_conversation","message":"Show details for Widget Mini"}]},"variant":"secondary"},"partial":false,"hasDynamicProps":true,"statementId":"p2btn"}}},"partial":false,"hasDynamicProps":true,"statementId":"p2"}],"layout":"grid","responsive":true},"partial":false,"hasDynamicProps":true,"statementId":"productCards"}]},"partial":false,"hasDynamicProps":true,"statementId":"root"},"errors":[]}}, +{"name":"openui-example-6","schema":"openui","input":"Example 6 — Editable table and a selection form:\n\nroot = Stack([tableHeader, editTable, formHeader, prefsForm])\ntableHeader = InlineHeader(\"Team Roster\", \"Click a cell to edit\")\neditTable = EditableTable(\"roster\", [colName, colRole, colStart], [row1, row2])\ncolName = { type: \"text\", key: \"name\", header: \"Name\" }\ncolRole = { type: \"select\", key: \"role\", header: \"Role\", options: [roleEng, roleDesign] }\nroleEng = { value: \"eng\", label: \"Engineering\" }\nroleDesign = { value: \"design\", label: \"Design\" }\ncolStart = { type: \"date-single\", key: \"start\", header: \"Start Date\" }\nrow1 = { id: \"1\", values: [\"Alex Kim\", \"eng\", \"2024-01-15\"] }\nrow2 = { id: \"2\", values: [\"Jamie Lee\", \"design\", \"2024-03-02\"] }\nformHeader = InlineHeader(\"Preferences\", \"Tell us how you like to work\")\nprefsForm = Form(\"prefs\", formButtons, [fc1, fc2])\nfc1 = FormControl(\"Working Style\", styleCards, \"Pick the one that fits best\")\nstyleCards = OptionCards(\"style\", \"single\", [style1, style2])\nstyle1 = OptionCard(\"focused\", \"Deep Focus\", \"Long uninterrupted blocks\", style1icon)\nstyle1icon = Icon(\"target\", \"tools\")\nstyle2 = OptionCard(\"collab\", \"Collaborative\", \"Frequent pairing and syncs\", style2icon)\nstyle2icon = Icon(\"users\", \"people\")\nfc2 = FormControl(\"Tools\", toolChips, \"Select all that apply\")\ntoolChips = Chips(\"tools\", \"multiple\", [tool1, tool2, tool3])\ntool1 = ChipItem(\"figma\", \"Figma\", tool1icon)\ntool1icon = Icon(\"figma\", \"design\")\ntool2 = ChipItem(\"slack\", \"Slack\", tool2icon)\ntool2icon = Icon(\"message-square\", \"communication\")\ntool3 = ChipItem(\"notion\", \"Notion\", tool3icon)\ntool3icon = Icon(\"notebook\", \"text\")\nformButtons = Buttons([Button(\"Save\", Action([@ToAssistant(\"Save preferences\")]), \"primary\")])","expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"InlineHeader","props":{"heading":"Team Roster","description":"Click a cell to edit"},"partial":false,"hasDynamicProps":false,"statementId":"tableHeader"},{"type":"element","typeName":"EditableTable","props":{"name":"roster","columns":[{"type":"text","key":"name","header":"Name"},{"type":"select","key":"role","header":"Role","options":[{"value":"eng","label":"Engineering"},{"value":"design","label":"Design"}]},{"type":"date-single","key":"start","header":"Start Date"}],"data":[{"id":"1","values":["Alex Kim","eng","2024-01-15"]},{"id":"2","values":["Jamie Lee","design","2024-03-02"]}]},"partial":false,"hasDynamicProps":false,"statementId":"editTable"},{"type":"element","typeName":"InlineHeader","props":{"heading":"Preferences","description":"Tell us how you like to work"},"partial":false,"hasDynamicProps":false,"statementId":"formHeader"},{"type":"element","typeName":"Form","props":{"name":"prefs","buttons":{"type":"element","typeName":"Buttons","props":{"buttons":[{"type":"element","typeName":"Button","props":{"label":"Save","action":{"steps":[{"type":"continue_conversation","message":"Save preferences"}]},"variant":"primary"},"partial":false,"hasDynamicProps":true}]},"partial":false,"hasDynamicProps":true,"statementId":"formButtons"},"fields":[{"type":"element","typeName":"FormControl","props":{"label":"Working Style","input":{"type":"element","typeName":"OptionCards","props":{"name":"style","type":"single","items":[{"type":"element","typeName":"OptionCard","props":{"value":"focused","title":"Deep Focus","subtitle":"Long uninterrupted blocks","topContent":{"type":"element","typeName":"Icon","props":{"name":"target","category":"tools"},"partial":false,"hasDynamicProps":false,"statementId":"style1icon"}},"partial":false,"hasDynamicProps":false,"statementId":"style1"},{"type":"element","typeName":"OptionCard","props":{"value":"collab","title":"Collaborative","subtitle":"Frequent pairing and syncs","topContent":{"type":"element","typeName":"Icon","props":{"name":"users","category":"people"},"partial":false,"hasDynamicProps":false,"statementId":"style2icon"}},"partial":false,"hasDynamicProps":false,"statementId":"style2"}]},"partial":false,"hasDynamicProps":false,"statementId":"styleCards"},"hint":"Pick the one that fits best"},"partial":false,"hasDynamicProps":false,"statementId":"fc1"},{"type":"element","typeName":"FormControl","props":{"label":"Tools","input":{"type":"element","typeName":"Chips","props":{"name":"tools","type":"multiple","items":[{"type":"element","typeName":"ChipItem","props":{"value":"figma","label":"Figma","icon":{"type":"element","typeName":"Icon","props":{"name":"figma","category":"design"},"partial":false,"hasDynamicProps":false,"statementId":"tool1icon"}},"partial":false,"hasDynamicProps":false,"statementId":"tool1"},{"type":"element","typeName":"ChipItem","props":{"value":"slack","label":"Slack","icon":{"type":"element","typeName":"Icon","props":{"name":"message-square","category":"communication"},"partial":false,"hasDynamicProps":false,"statementId":"tool2icon"}},"partial":false,"hasDynamicProps":false,"statementId":"tool2"},{"type":"element","typeName":"ChipItem","props":{"value":"notion","label":"Notion","icon":{"type":"element","typeName":"Icon","props":{"name":"notebook","category":"text"},"partial":false,"hasDynamicProps":false,"statementId":"tool3icon"}},"partial":false,"hasDynamicProps":false,"statementId":"tool3"}]},"partial":false,"hasDynamicProps":false,"statementId":"toolChips"},"hint":"Select all that apply"},"partial":false,"hasDynamicProps":false,"statementId":"fc2"}]},"partial":false,"hasDynamicProps":true,"statementId":"prefsForm"}]},"partial":false,"hasDynamicProps":true,"statementId":"root"},"errors":[]}}, +{"name":"operators","schema":"test","input":"$a = 1\n$b = \"2\"\nroot = Card([Text($a + $b * 2 - -1 > 3 && !$c || $a == \"1\" ? \"yes\" : \"no\")])","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"yes"},"partial":false,"hasDynamicProps":true}]},"partial":false,"hasDynamicProps":true,"statementId":"root"},"errors":[]}}, +{"name":"member-index","schema":"test","input":"$data = {rows: [{name: \"a\"}, {name: \"b\"}], \"key x\": 1}\nroot = Card([Text($data.rows[1].name), Text($data.rows.name), Text($data[\"key x\"])])","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"b"},"partial":false,"hasDynamicProps":true},{"type":"element","typeName":"Text","props":{"text":["a","b"]},"partial":false,"hasDynamicProps":true},{"type":"element","typeName":"Text","props":{"text":1},"partial":false,"hasDynamicProps":true}]},"partial":false,"hasDynamicProps":true,"statementId":"root"},"errors":[]}}, +{"name":"actions","schema":"test","input":"$n = 0\nroot = Card([b1, b2, b3])\nb1 = Button(\"Inc\", Action([@Set($n, $n + 1), @ToAssistant(\"Bumped\", \"ctx\")]))\nb2 = Button(\"Reset\", Action([@Reset($n)]))\nb3 = Button(\"Docs\", Action([@OpenUrl(\"https://openui.com\")]))","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Button","props":{"label":"Inc","action":{"steps":[{"type":"set","target":"$n","valueAST":{"k":"BinOp","op":"+","left":{"k":"StateRef","n":"$n"},"right":{"k":"Num","v":1}}},{"type":"continue_conversation","message":"Bumped","context":"ctx"}]}},"partial":false,"hasDynamicProps":true,"statementId":"b1"},{"type":"element","typeName":"Button","props":{"label":"Reset","action":{"steps":[{"type":"reset","targets":["$n"]}]}},"partial":false,"hasDynamicProps":true,"statementId":"b2"},{"type":"element","typeName":"Button","props":{"label":"Docs","action":{"steps":[{"type":"open_url","url":"https://openui.com"}]}},"partial":false,"hasDynamicProps":true,"statementId":"b3"}]},"partial":false,"hasDynamicProps":true,"statementId":"root"},"errors":[]}}, +{"name":"each","schema":"test","input":"$todos = [{title: \"a\", done: false}, {title: \"b\", done: true}]\nroot = Card(@Each($todos, todo, Text(todo.title)))","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"a"},"partial":false,"hasDynamicProps":true},{"type":"element","typeName":"Text","props":{"text":"b"},"partial":false,"hasDynamicProps":true}]},"partial":false,"hasDynamicProps":true,"statementId":"root"},"errors":[]}}, +{"name":"builtins-all","schema":"test","input":"$n = [5, \"3\", 9, \"x\", true]\n$rows = [{n: \"b\", v: 2}, {n: \"a\", v: 10}, {n: \"c\", v: \"1\"}]\nroot = Card([Text(\"\" + @Count($n) + \"|\" + @Sum($n) + \"|\" + @Avg($n) + \"|\" + @Min($n) + \"|\" + @Max($n) + \"|\" + @First($n) + \"|\" + @Last($n) + \"|\" + @Round(-2.5) + \"|\" + @Round(1.005, 2) + \"|\" + @Abs(-3) + \"|\" + @Floor(1.7) + \"|\" + @Ceil(1.2)), Text(@Sort($rows, \"v\", \"desc\").n + \"\"), Text(@Sort($rows, \"n\").n + \"\"), Text(@Filter($rows, \"v\", \">\", 1).n + \"\"), Text(@Filter($rows, \"n\", \"contains\", \"a\").n + \"\"), Text(@Filter($rows, \"v\", \"==\", \"2\").n + \"\")])","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"5|18|3.6|0|9|5|true|-2|1|3|1|2"},"partial":false,"hasDynamicProps":true},{"type":"element","typeName":"Text","props":{"text":"a,b,c"},"partial":false,"hasDynamicProps":true},{"type":"element","typeName":"Text","props":{"text":"a,b,c"},"partial":false,"hasDynamicProps":true},{"type":"element","typeName":"Text","props":{"text":"b,a"},"partial":false,"hasDynamicProps":true},{"type":"element","typeName":"Text","props":{"text":"a"},"partial":false,"hasDynamicProps":true},{"type":"element","typeName":"Text","props":{"text":"b"},"partial":false,"hasDynamicProps":true}]},"partial":false,"hasDynamicProps":true,"statementId":"root"},"errors":[]}}, +{"name":"arithmetic-edges","schema":"test","input":"$z = 0\nroot = Card([Text(\"\" + (5 / $z) + \"|\" + (-7 % 3) + \"|\" + (7 % $z) + \"|\" + (\"a\" + null) + \"|\" + (null + 1) + \"|\" + (true + 1) + \"|\" + (\"5\" * \"2\") + \"|\" + (\"x\" - 1) + \"|\" + (0.1 + 0.2) + \"|\" + (1 / 3))])","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"0|-1|0|a|1|2|10|-1|0.30000000000000004|0.3333333333333333"},"partial":false,"hasDynamicProps":true}]},"partial":false,"hasDynamicProps":true,"statementId":"root"},"errors":[]}}, +{"name":"truthiness-and-equality","schema":"test","input":"$e = \"\"\n$z = 0\n$arr = []\nroot = Card([Text(\"\" + ($e || \"empty\") + \"|\" + ($z && \"no\") + \"|\" + ($arr ? \"arr-truthy\" : \"arr-falsy\") + \"|\" + (1 == \"1\") + \"|\" + (null == 0) + \"|\" + ($missing == null) + \"|\" + !$e)])","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"empty|0|arr-truthy|true|false|true|true"},"partial":false,"hasDynamicProps":true}]},"partial":false,"hasDynamicProps":true,"statementId":"root"},"errors":[]}}, +{"name":"ternary-components","schema":"test","input":"$on = true\nroot = Card([$on ? Text(\"on\", \"l\") : Button(\"off\"), $on ? null : Text(\"hidden\")])","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"on","size":"l"},"partial":false,"hasDynamicProps":true},null]},"partial":false,"hasDynamicProps":true,"statementId":"root"},"errors":[]}}, +{"name":"ternary-components-off","schema":"test","input":"$on = true\nroot = Card([$on ? Text(\"on\", \"l\") : Button(\"off\")])","state":{"$on":false},"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Button","props":{"label":"off"},"partial":false,"hasDynamicProps":true}]},"partial":false,"hasDynamicProps":true,"statementId":"root"},"errors":[]}}, +{"name":"state-override","schema":"test","input":"$count = 1\nroot = Card([Text(\"count \" + $count)])","state":{"$count":41},"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"count 41"},"partial":false,"hasDynamicProps":true}]},"partial":false,"hasDynamicProps":true,"statementId":"root"},"errors":[]}}, +{"name":"reactive-binding","schema":"test","input":"$v = \"hi\"\nroot = Card([Input(\"name\", $v), Input(\"plain\", \"x\")])","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Input","props":{"name":"name","value":{"__reactive":"assign","target":"$v","expr":{"k":"StateRef","n":"$value"}}},"partial":false,"hasDynamicProps":true},{"type":"element","typeName":"Input","props":{"name":"plain","value":"x"},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":true,"statementId":"root"},"errors":[]}}, +{"name":"reactive-assign","schema":"test","input":"$v = \"\"\nroot = Card([Input(\"name\", $v = $value)])","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Input","props":{"name":"name","value":{"__reactive":"assign","target":"$v","expr":{"k":"StateRef","n":"$value"}}},"partial":false,"hasDynamicProps":true}]},"partial":false,"hasDynamicProps":true,"statementId":"root"},"errors":[]}}, +{"name":"query-results","schema":"test","input":"$q = \"x\"\nusers = Query(\"list_users\", {search: $q, page: $page}, {rows: []}, 30)\nsave = Mutation(\"save_user\", {name: $q})\nroot = Card([Text(users.rows.length + \" users\"), Button(\"Save\", Action([@Run(save), @Run(users)]))])","queryResults":{"users":{"rows":[{"id":1},{"id":2}]},"save":{"status":"idle"}},"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"2 users"},"partial":false,"hasDynamicProps":true},{"type":"element","typeName":"Button","props":{"label":"Save","action":{"steps":[{"type":"run","statementId":"save","refType":"mutation"},{"type":"run","statementId":"users","refType":"query"}]}},"partial":false,"hasDynamicProps":true}]},"partial":false,"hasDynamicProps":true,"statementId":"root"},"errors":[]}}, +{"name":"each-actions","schema":"test","input":"$items = [{id: 1, t: \"a\"}, {id: 2, t: \"b\"}]\n$sel = 0\nroot = Card(@Each($items, it, Button(it.t, Action([@Set($sel, it.id), @ToAssistant(\"Pick \" + it.t)]))))","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Button","props":{"label":"a","action":{"steps":[{"type":"set","target":"$sel","valueAST":{"k":"Num","v":1}},{"type":"continue_conversation","message":"Pick a"}]}},"partial":false,"hasDynamicProps":true},{"type":"element","typeName":"Button","props":{"label":"b","action":{"steps":[{"type":"set","target":"$sel","valueAST":{"k":"Num","v":2}},{"type":"continue_conversation","message":"Pick b"}]}},"partial":false,"hasDynamicProps":true}]},"partial":false,"hasDynamicProps":true,"statementId":"root"},"errors":[]}} +] diff --git a/packages/swift-lang/Tests/OpenUILangTests/Fixtures/merge.json b/packages/swift-lang/Tests/OpenUILangTests/Fixtures/merge.json new file mode 100644 index 0000000000..dd0d3392e4 --- /dev/null +++ b/packages/swift-lang/Tests/OpenUILangTests/Fixtures/merge.json @@ -0,0 +1,21 @@ +[ +{"name":"empty-existing","existing":"","patch":"root = Card([a])\na = TextContent(\"x\")","expected":"root = Card([a])\na = TextContent(\"x\")"}, +{"name":"empty-patch","existing":"root = Card([title, body, $filter], \"Report\")\ntitle = TextContent(\"Q3\", \"large\")\nbody = Table([colA, colB])\ncolA = Col(\"Region\", [\"EU\", \"US\"])\ncolB = Col(\"Revenue\", [1, 2])\n$filter = \"all\"","patch":" \n","expected":"root = Card([title, body, $filter], \"Report\")\ntitle = TextContent(\"Q3\", \"large\")\nbody = Table([colA, colB])\ncolA = Col(\"Region\", [\"EU\", \"US\"])\ncolB = Col(\"Revenue\", [1, 2])\n$filter = \"all\""}, +{"name":"replace","existing":"root = Card([title, body, $filter], \"Report\")\ntitle = TextContent(\"Q3\", \"large\")\nbody = Table([colA, colB])\ncolA = Col(\"Region\", [\"EU\", \"US\"])\ncolB = Col(\"Revenue\", [1, 2])\n$filter = \"all\"","patch":"title = TextContent(\"Q4\", \"large\")","expected":"root = Card([title, body, $filter], \"Report\")\ntitle = TextContent(\"Q4\", \"large\")\nbody = Table([colA, colB])\ncolA = Col(\"Region\", [\"EU\", \"US\"])\ncolB = Col(\"Revenue\", [1, 2])\n$filter = \"all\""}, +{"name":"add-referenced","existing":"root = Card([title, body, $filter], \"Report\")\ntitle = TextContent(\"Q3\", \"large\")\nbody = Table([colA, colB])\ncolA = Col(\"Region\", [\"EU\", \"US\"])\ncolB = Col(\"Revenue\", [1, 2])\n$filter = \"all\"","patch":"root = Card([title, body, note])\nnote = Callout(\"info\", \"Hi\")","expected":"root = Card([title, body, note])\ntitle = TextContent(\"Q3\", \"large\")\nbody = Table([colA, colB])\ncolA = Col(\"Region\", [\"EU\", \"US\"])\ncolB = Col(\"Revenue\", [1, 2])\n$filter = \"all\"\nnote = Callout(\"info\", \"Hi\")"}, +{"name":"add-unreferenced","existing":"root = Card([title, body, $filter], \"Report\")\ntitle = TextContent(\"Q3\", \"large\")\nbody = Table([colA, colB])\ncolA = Col(\"Region\", [\"EU\", \"US\"])\ncolB = Col(\"Revenue\", [1, 2])\n$filter = \"all\"","patch":"orphan = TextContent(\"never shown\")","expected":"root = Card([title, body, $filter], \"Report\")\ntitle = TextContent(\"Q3\", \"large\")\nbody = Table([colA, colB])\ncolA = Col(\"Region\", [\"EU\", \"US\"])\ncolB = Col(\"Revenue\", [1, 2])\n$filter = \"all\""}, +{"name":"delete","existing":"root = Card([title, body, $filter], \"Report\")\ntitle = TextContent(\"Q3\", \"large\")\nbody = Table([colA, colB])\ncolA = Col(\"Region\", [\"EU\", \"US\"])\ncolB = Col(\"Revenue\", [1, 2])\n$filter = \"all\"","patch":"body = null\nroot = Card([title])","expected":"root = Card([title])\ntitle = TextContent(\"Q3\", \"large\")\n$filter = \"all\""}, +{"name":"gc-after-replace","existing":"root = Card([title, body, $filter], \"Report\")\ntitle = TextContent(\"Q3\", \"large\")\nbody = Table([colA, colB])\ncolA = Col(\"Region\", [\"EU\", \"US\"])\ncolB = Col(\"Revenue\", [1, 2])\n$filter = \"all\"","patch":"body = TextContent(\"no table\")","expected":"root = Card([title, body, $filter], \"Report\")\ntitle = TextContent(\"Q3\", \"large\")\nbody = TextContent(\"no table\")\n$filter = \"all\""}, +{"name":"keeps-unused-state","existing":"root = Card([title, body, $filter], \"Report\")\ntitle = TextContent(\"Q3\", \"large\")\nbody = Table([colA, colB])\ncolA = Col(\"Region\", [\"EU\", \"US\"])\ncolB = Col(\"Revenue\", [1, 2])\n$filter = \"all\"","patch":"root = Card([title])\n$extra = 1","expected":"root = Card([title])\ntitle = TextContent(\"Q3\", \"large\")\n$filter = \"all\"\n$extra = 1"}, +{"name":"root-deleted","existing":"root = Card([title, body, $filter], \"Report\")\ntitle = TextContent(\"Q3\", \"large\")\nbody = Table([colA, colB])\ncolA = Col(\"Region\", [\"EU\", \"US\"])\ncolB = Col(\"Revenue\", [1, 2])\n$filter = \"all\"","patch":"root = null","expected":"title = TextContent(\"Q3\", \"large\")\nbody = Table([colA, colB])\ncolA = Col(\"Region\", [\"EU\", \"US\"])\ncolB = Col(\"Revenue\", [1, 2])\n$filter = \"all\""}, +{"name":"fenced-patch","existing":"root = Card([title, body, $filter], \"Report\")\ntitle = TextContent(\"Q3\", \"large\")\nbody = Table([colA, colB])\ncolA = Col(\"Region\", [\"EU\", \"US\"])\ncolB = Col(\"Revenue\", [1, 2])\n$filter = \"all\"","patch":"```openui\ntitle = TextContent(\"Fenced\")\n```","expected":"root = Card([title, body, $filter], \"Report\")\ntitle = TextContent(\"Fenced\")\nbody = Table([colA, colB])\ncolA = Col(\"Region\", [\"EU\", \"US\"])\ncolB = Col(\"Revenue\", [1, 2])\n$filter = \"all\""}, +{"name":"multiline-and-strings","existing":"root = Card([title, body, $filter], \"Report\")\ntitle = TextContent(\"Q3\", \"large\")\nbody = Table([colA, colB])\ncolA = Col(\"Region\", [\"EU\", \"US\"])\ncolB = Col(\"Revenue\", [1, 2])\n$filter = \"all\"","patch":"body = Table([\n colA,\n colB\n])\ntitle = TextContent(\"a ) ] } \\\" ' \\n\", \"large\")","expected":"root = Card([title, body, $filter], \"Report\")\ntitle = TextContent(\"a ) ] } \\\" ' \\n\", \"large\")\nbody = Table([\n colA,\n colB\n])\ncolA = Col(\"Region\", [\"EU\", \"US\"])\ncolB = Col(\"Revenue\", [1, 2])\n$filter = \"all\""}, +{"name":"single-quotes","existing":"root = Card([title, body, $filter], \"Report\")\ntitle = TextContent(\"Q3\", \"large\")\nbody = Table([colA, colB])\ncolA = Col(\"Region\", [\"EU\", \"US\"])\ncolB = Col(\"Revenue\", [1, 2])\n$filter = \"all\"","patch":"title = TextContent('it\\'s (fine')","expected":"root = Card([title, body, $filter], \"Report\")\ntitle = TextContent('it\\'s (fine')\nbody = Table([colA, colB])\ncolA = Col(\"Region\", [\"EU\", \"US\"])\ncolB = Col(\"Revenue\", [1, 2])\n$filter = \"all\""}, +{"name":"crlf","existing":"root = Card([title, body, $filter], \"Report\")\r\ntitle = TextContent(\"Q3\", \"large\")\r\nbody = Table([colA, colB])\r\ncolA = Col(\"Region\", [\"EU\", \"US\"])\r\ncolB = Col(\"Revenue\", [1, 2])\r\n$filter = \"all\"","patch":"title = TextContent(\"CRLF\")\r\n","expected":"root = Card([title, body, $filter], \"Report\")\ntitle = TextContent(\"CRLF\")\nbody = Table([colA, colB])\ncolA = Col(\"Region\", [\"EU\", \"US\"])\ncolB = Col(\"Revenue\", [1, 2])\n$filter = \"all\""}, +{"name":"duplicate-existing","existing":"root = Card([title, body, $filter], \"Report\")\ntitle = TextContent(\"Q3\", \"large\")\nbody = Table([colA, colB])\ncolA = Col(\"Region\", [\"EU\", \"US\"])\ncolB = Col(\"Revenue\", [1, 2])\n$filter = \"all\"\ntitle = TextContent(\"dup\")","patch":"colA = Col(\"Area\", [])","expected":"root = Card([title, body, $filter], \"Report\")\ntitle = TextContent(\"dup\")\nbody = Table([colA, colB])\ncolA = Col(\"Area\", [])\ncolB = Col(\"Revenue\", [1, 2])\n$filter = \"all\"\ntitle = TextContent(\"dup\")"}, +{"name":"delete-then-add","existing":"root = Card([title, body, $filter], \"Report\")\ntitle = TextContent(\"Q3\", \"large\")\nbody = Table([colA, colB])\ncolA = Col(\"Region\", [\"EU\", \"US\"])\ncolB = Col(\"Revenue\", [1, 2])\n$filter = \"all\"","patch":"title = null\ntitle = TextContent(\"back\")","expected":"root = Card([title, body, $filter], \"Report\")\nbody = Table([colA, colB])\ncolA = Col(\"Region\", [\"EU\", \"US\"])\ncolB = Col(\"Revenue\", [1, 2])\n$filter = \"all\"\ntitle = TextContent(\"back\")"}, +{"name":"query-refs","existing":"root = Card([t])\nt = Table([c])\nc = Col(\"n\", q.rows)\nq = Query(\"list\", {}, {rows: []})","patch":"c = Col(\"name\", q.rows)","expected":"root = Card([t])\nt = Table([c])\nc = Col(\"name\", q.rows)\nq = Query(\"list\", {}, {rows: []})"}, +{"name":"custom-root","existing":"main = Card([a])\na = TextContent(\"x\")\nb = TextContent(\"y\")","patch":"a = TextContent(\"z\")","rootId":"main","expected":"main = Card([a])\na = TextContent(\"z\")"}, +{"name":"unclosed-patch","existing":"root = Card([title, body, $filter], \"Report\")\ntitle = TextContent(\"Q3\", \"large\")\nbody = Table([colA, colB])\ncolA = Col(\"Region\", [\"EU\", \"US\"])\ncolB = Col(\"Revenue\", [1, 2])\n$filter = \"all\"","patch":"title = TextContent(\"cut","expected":"root = Card([title, body, $filter], \"Report\")\ntitle = TextContent(\"cut\nbody = Table([colA, colB])\ncolA = Col(\"Region\", [\"EU\", \"US\"])\ncolB = Col(\"Revenue\", [1, 2])\n$filter = \"all\""}, +{"name":"chat-example","existing":"Example 1 — Informational response (the shape for \"tell me about X\" / explainer asks):\n\nroot = Card([header, intro, highlights, statsHeader, statsCards, sectionsBlock, actionBtns], [src1, src2])\nheader = CardHeader(\"Paris\", \"The City of Light · France's Eternal Capital\")\nintro = TextContent(\"Paris is one of the world's most visited cities — a timeless blend of iconic landmarks, world-class cuisine, art, and romance [1]. In 2026 the city is buzzing with new energy: the **Grand Palais** has reopened after a €466M renovation, and you can now **swim in the Seine** for the first time in over a century [2].\")\nhighlights = ContextCardBlock([h1, h2, h3], \"grid\", true, { type: \"continue_conversation\", context: \"Tell me more about this aspect of Paris\" })\nh1 = ContextCardItem(\"art-museums\", \"Art & Museums\", \"143 museums including the Louvre & newly reopened Grand Palais.\", \"gray\", \"https://cdn.britannica.com/03/121003-050-2544BD4E/Interior-Louvre-Museum-Paris.jpg\")\nh2 = ContextCardItem(\"gastronomy\", \"Gastronomy\", \"9,000+ restaurants, 130+ Michelin stars, and the world's best baguettes.\", \"gray\")\nh3 = ContextCardItem(\"fashion-capital\", \"Fashion Capital\", \"Home to Chanel, Dior, Louis Vuitton & Hermès — the global luxury hub.\", \"gray\", \"https://thumbs.dreamstime.com/z/dior-storefront-facade-chanel-store-lvmh-s-french-designer-modehouse-christian-adjacent-to-luxury-fashion-beauty-brand-327161553.jpg\")\nstatsHeader = InlineHeader(\"Paris by the Numbers\", \"Key visitor statistics\")\nstatsCards = OverviewCardBlock([ov1, ov2, ov3], \"grid\", true)\nov1 = OverviewCardItem(\"annual-visitors\", ov1top, ov1metric)\nov1top = IconText(ov1icon, \"neutral\", \"m\", \"Annual Visitors\", \"2024 arrivals\", false, \"vertical\")\nov1icon = Icon(\"users\", \"people\")\nov1metric = MetricIndicatorInline(\"48.7M\", \"tourists\", { direction: \"up\", value: 2.5 })\nov2 = OverviewCardItem(\"museums-monuments\", ov2top, ov2metric)\nov2top = IconText(ov2icon, \"neutral\", \"m\", \"Museums & Monuments\", \"Cultural sites\", false, \"vertical\")\nov2icon = Icon(\"landmark\", \"buildings\")\nov2metric = MetricIndicatorInline(\"2,370+\", \"across the city\")\nov3 = OverviewCardItem(\"tourism-revenue\", ov3top, ov3metric)\nov3top = IconText(ov3icon, \"neutral\", \"m\", \"Tourism Revenue\", \"2024 estimate\", false, \"vertical\")\nov3icon = Icon(\"circle-dollar-sign\", \"finance\")\nov3metric = MetricIndicatorInline(\"€23.4B\", \"generated\")\nsectionsBlock = SectionBlock([secLandmarks, secFood, secWhen], false)\nsecLandmarks = SectionItem(\"landmarks\", \"Iconic Landmarks\", [landmarksCarousel])\nlandmarksCarousel = VisualCardBlock([lm1, lm2, lm3, lm4], \"carousel\", true, { type: \"continue_conversation\", context: \"Tell me more about this Paris landmark\" })\nlm1 = VisualCardItem(lm1body, \"eiffel-tower\", \"https://c8.alamy.com/comp/RYEB13/aerial-view-of-the-eiffel-tower-with-the-park-champ-de-mars-and-the-river-seine-paris-france-RYEB13.jpg\", lm1tag, \"Aerial view of the Eiffel Tower\")\nlm1body = BoldText(\"text\", \"Eiffel Tower\", \"Open daily 9am–midnight\")\nlm1tag = Tag(\"Must-See\", lm1tagIcon, \"sm\", \"info\")\nlm1tagIcon = Icon(\"star\", \"shapes\")\nlm2 = VisualCardItem(lm2body, \"the-louvre\", \"https://images.fineartamerica.com/images/artworkimages/mediumlarge/3/1-exterior-of-musee-du-louvre-museum-paris-france-bernard-jaubert.jpg\", lm2tag, \"Exterior of the Louvre museum\")\nlm2body = BoldText(\"text\", \"The Louvre\", \"World's largest art museum\")\nlm2tag = Tag(\"8.9M visits/yr\", lm2tagIcon, \"sm\", \"success\")\nlm2tagIcon = Icon(\"trending-up\", \"charts\")\nlm3 = VisualCardItem(lm3body, \"notre-dame\", \"https://cdn.britannica.com/29/255529-050-63A22A3C/notre-dame-de-paris-cathedral-paris-france.jpg\", lm3tag, \"Notre-Dame cathedral facade\")\nlm3body = BoldText(\"text\", \"Notre-Dame\", \"Fully restored cathedral\")\nlm3tag = Tag(\"Reopened 2024\", lm3tagIcon, \"sm\", \"warning\")\nlm3tagIcon = Icon(\"hammer\", \"tools\")\nlm4 = VisualCardItem(lm4body, \"versailles\", \"https://cdn.britannica.com/76/130076-050-31128A3D/Latona-Fountain-Balthazard-Marsy-Gaspard-Andre-Le.jpg\", lm4tag, \"Latona Fountain at Versailles\")\nlm4body = BoldText(\"text\", \"Palace of Versailles\", \"Royal gardens & grand halls\")\nlm4tag = Tag(\"Day Trip\", lm4tagIcon, \"sm\", \"neutral\")\nlm4tagIcon = Icon(\"train-front\", \"travel\")\nsecFood = SectionItem(\"food\", \"Food & Drink\", [foodCards])\nfoodCards = ContextCardBlock([f1, f2, f3], \"grid\", true, { type: \"continue_conversation\", context: \"Tell me more about eating and drinking in Paris\" })\nf1 = ContextCardItem(\"bistros\", \"Bistros\", \"Classic French fare — try Les Arlots for confit beef cheeks.\", \"gray\", \"https://everydayparisian.com/wp-content/uploads/2023/01/IMG_0221-768x1024.webp\")\nf2 = ContextCardItem(\"fine-dining\", \"Fine Dining\", \"Septime leads a new wave of creative Michelin-starred cuisine.\", \"gray\", \"https://production-data.worldofmouth.app/images/72b2e6db-97ea-49c4-aa5d-3b7c8121c812.jpg\")\nf3 = ContextCardItem(\"bakeries\", \"Bakeries\", \"Shinya Pain for sourdough. Grand Prix-winning baguettes citywide.\", \"gray\", \"https://www.davidlebovitz.com/wp-content/uploads/2018/09/Le-petit-grain-paris-bakery-boulangerie-patisserie-pastry-shop-8-640x895.jpg\")\nsecWhen = SectionItem(\"when\", \"Best Time to Visit\", [whenTabs])\nwhenTabs = Tabs([tabSpring, tabSummer])\ntabSpring = TabItem(\"spring\", \"Spring\", [springList])\nspringList = ListBlock([sp1, sp2, sp3], \"number\")\nsp1 = ListItem(\"Cherry blossoms at Parc de Sceaux\", \"March – May\", null, null, { type: \"continue_conversation\", context: \"Tell me about cherry blossom season in Paris\" })\nsp2 = ListItem(\"Picnics along the Seine & in city parks\", \"Warm, lively atmosphere\", null, null, { type: \"continue_conversation\", context: \"Where are the best picnic spots in Paris?\" })\nsp3 = ListItem(\"Fewer crowds than summer\", \"Ideal for sightseeing\", null, null, { type: \"continue_conversation\", context: \"How busy is Paris in spring?\" })\ntabSummer = TabItem(\"summer\", \"Summer\", [summerList])\nsummerList = ListBlock([su1, su2, su3], \"number\")\nsu1 = ListItem(\"Swim in the Seine — open July & August 2026\", \"First time in 102 years\", null, null, { type: \"continue_conversation\", context: \"Tell me about swimming in the Seine\" })\nsu2 = ListItem(\"Open-air cinema at Parc de la Villette\", \"Free films under the stars\", null, null, { type: \"continue_conversation\", context: \"Tell me about the open-air cinema at Parc de la Villette\" })\nsu3 = ListItem(\"Bastille Day celebrations on July 14\", \"Fireworks at the Eiffel Tower\", null, null, { type: \"continue_conversation\", context: \"What happens in Paris on Bastille Day?\" })\nactionBtns = Buttons([btn1, btn2, btn3], \"row\")\nbtn1 = Button(\"Plan a Trip to Paris\", { type: \"continue_conversation\", context: \"Help me plan a trip to Paris\" }, \"primary\")\nbtn2 = Button(\"Best Hotels in Paris\", { type: \"continue_conversation\", context: \"Show me the best hotels in Paris\" }, \"secondary\")\nbtn3 = Button(\"Build a Paris Itinerary\", { type: \"continue_conversation\", context: \"Build a 5-day Paris itinerary for me\" }, \"secondary\")\nsrc1 = { title: \"Paris Travel Guide 2025\", sourceName: \"Travel and Tour World\", url: \"https://www.travelandtourworld.com/news/article/from-iconic-landmarks-to-hidden-gems-the-ultimate-paris-travel-guide-for-2025-you-need-to-read-now/\" }\nsrc2 = { title: \"Best Things to Do in Paris 2026\", sourceName: \"Time Out Paris\", url: \"http://www.timeout.fr/paris/en/for-tourists\" }","patch":"header = CardHeader(\"Edited\", \"by a patch\")","expected":"root = Card([header, intro, highlights, statsHeader, statsCards, sectionsBlock, actionBtns], [src1, src2])\nheader = CardHeader(\"Edited\", \"by a patch\")\nintro = TextContent(\"Paris is one of the world's most visited cities — a timeless blend of iconic landmarks, world-class cuisine, art, and romance [1]. In 2026 the city is buzzing with new energy: the **Grand Palais** has reopened after a €466M renovation, and you can now **swim in the Seine** for the first time in over a century [2].\")\nhighlights = ContextCardBlock([h1, h2, h3], \"grid\", true, { type: \"continue_conversation\", context: \"Tell me more about this aspect of Paris\" })\nh1 = ContextCardItem(\"art-museums\", \"Art & Museums\", \"143 museums including the Louvre & newly reopened Grand Palais.\", \"gray\", \"https://cdn.britannica.com/03/121003-050-2544BD4E/Interior-Louvre-Museum-Paris.jpg\")\nh2 = ContextCardItem(\"gastronomy\", \"Gastronomy\", \"9,000+ restaurants, 130+ Michelin stars, and the world's best baguettes.\", \"gray\")\nh3 = ContextCardItem(\"fashion-capital\", \"Fashion Capital\", \"Home to Chanel, Dior, Louis Vuitton & Hermès — the global luxury hub.\", \"gray\", \"https://thumbs.dreamstime.com/z/dior-storefront-facade-chanel-store-lvmh-s-french-designer-modehouse-christian-adjacent-to-luxury-fashion-beauty-brand-327161553.jpg\")\nstatsHeader = InlineHeader(\"Paris by the Numbers\", \"Key visitor statistics\")\nstatsCards = OverviewCardBlock([ov1, ov2, ov3], \"grid\", true)\nov1 = OverviewCardItem(\"annual-visitors\", ov1top, ov1metric)\nov1top = IconText(ov1icon, \"neutral\", \"m\", \"Annual Visitors\", \"2024 arrivals\", false, \"vertical\")\nov1icon = Icon(\"users\", \"people\")\nov1metric = MetricIndicatorInline(\"48.7M\", \"tourists\", { direction: \"up\", value: 2.5 })\nov2 = OverviewCardItem(\"museums-monuments\", ov2top, ov2metric)\nov2top = IconText(ov2icon, \"neutral\", \"m\", \"Museums & Monuments\", \"Cultural sites\", false, \"vertical\")\nov2icon = Icon(\"landmark\", \"buildings\")\nov2metric = MetricIndicatorInline(\"2,370+\", \"across the city\")\nov3 = OverviewCardItem(\"tourism-revenue\", ov3top, ov3metric)\nov3top = IconText(ov3icon, \"neutral\", \"m\", \"Tourism Revenue\", \"2024 estimate\", false, \"vertical\")\nov3icon = Icon(\"circle-dollar-sign\", \"finance\")\nov3metric = MetricIndicatorInline(\"€23.4B\", \"generated\")\nsectionsBlock = SectionBlock([secLandmarks, secFood, secWhen], false)\nsecLandmarks = SectionItem(\"landmarks\", \"Iconic Landmarks\", [landmarksCarousel])\nlandmarksCarousel = VisualCardBlock([lm1, lm2, lm3, lm4], \"carousel\", true, { type: \"continue_conversation\", context: \"Tell me more about this Paris landmark\" })\nlm1 = VisualCardItem(lm1body, \"eiffel-tower\", \"https://c8.alamy.com/comp/RYEB13/aerial-view-of-the-eiffel-tower-with-the-park-champ-de-mars-and-the-river-seine-paris-france-RYEB13.jpg\", lm1tag, \"Aerial view of the Eiffel Tower\")\nlm1body = BoldText(\"text\", \"Eiffel Tower\", \"Open daily 9am–midnight\")\nlm1tag = Tag(\"Must-See\", lm1tagIcon, \"sm\", \"info\")\nlm1tagIcon = Icon(\"star\", \"shapes\")\nlm2 = VisualCardItem(lm2body, \"the-louvre\", \"https://images.fineartamerica.com/images/artworkimages/mediumlarge/3/1-exterior-of-musee-du-louvre-museum-paris-france-bernard-jaubert.jpg\", lm2tag, \"Exterior of the Louvre museum\")\nlm2body = BoldText(\"text\", \"The Louvre\", \"World's largest art museum\")\nlm2tag = Tag(\"8.9M visits/yr\", lm2tagIcon, \"sm\", \"success\")\nlm2tagIcon = Icon(\"trending-up\", \"charts\")\nlm3 = VisualCardItem(lm3body, \"notre-dame\", \"https://cdn.britannica.com/29/255529-050-63A22A3C/notre-dame-de-paris-cathedral-paris-france.jpg\", lm3tag, \"Notre-Dame cathedral facade\")\nlm3body = BoldText(\"text\", \"Notre-Dame\", \"Fully restored cathedral\")\nlm3tag = Tag(\"Reopened 2024\", lm3tagIcon, \"sm\", \"warning\")\nlm3tagIcon = Icon(\"hammer\", \"tools\")\nlm4 = VisualCardItem(lm4body, \"versailles\", \"https://cdn.britannica.com/76/130076-050-31128A3D/Latona-Fountain-Balthazard-Marsy-Gaspard-Andre-Le.jpg\", lm4tag, \"Latona Fountain at Versailles\")\nlm4body = BoldText(\"text\", \"Palace of Versailles\", \"Royal gardens & grand halls\")\nlm4tag = Tag(\"Day Trip\", lm4tagIcon, \"sm\", \"neutral\")\nlm4tagIcon = Icon(\"train-front\", \"travel\")\nsecFood = SectionItem(\"food\", \"Food & Drink\", [foodCards])\nfoodCards = ContextCardBlock([f1, f2, f3], \"grid\", true, { type: \"continue_conversation\", context: \"Tell me more about eating and drinking in Paris\" })\nf1 = ContextCardItem(\"bistros\", \"Bistros\", \"Classic French fare — try Les Arlots for confit beef cheeks.\", \"gray\", \"https://everydayparisian.com/wp-content/uploads/2023/01/IMG_0221-768x1024.webp\")\nf2 = ContextCardItem(\"fine-dining\", \"Fine Dining\", \"Septime leads a new wave of creative Michelin-starred cuisine.\", \"gray\", \"https://production-data.worldofmouth.app/images/72b2e6db-97ea-49c4-aa5d-3b7c8121c812.jpg\")\nf3 = ContextCardItem(\"bakeries\", \"Bakeries\", \"Shinya Pain for sourdough. Grand Prix-winning baguettes citywide.\", \"gray\", \"https://www.davidlebovitz.com/wp-content/uploads/2018/09/Le-petit-grain-paris-bakery-boulangerie-patisserie-pastry-shop-8-640x895.jpg\")\nsecWhen = SectionItem(\"when\", \"Best Time to Visit\", [whenTabs])\nwhenTabs = Tabs([tabSpring, tabSummer])\ntabSpring = TabItem(\"spring\", \"Spring\", [springList])\nspringList = ListBlock([sp1, sp2, sp3], \"number\")\nsp1 = ListItem(\"Cherry blossoms at Parc de Sceaux\", \"March – May\", null, null, { type: \"continue_conversation\", context: \"Tell me about cherry blossom season in Paris\" })\nsp2 = ListItem(\"Picnics along the Seine & in city parks\", \"Warm, lively atmosphere\", null, null, { type: \"continue_conversation\", context: \"Where are the best picnic spots in Paris?\" })\nsp3 = ListItem(\"Fewer crowds than summer\", \"Ideal for sightseeing\", null, null, { type: \"continue_conversation\", context: \"How busy is Paris in spring?\" })\ntabSummer = TabItem(\"summer\", \"Summer\", [summerList])\nsummerList = ListBlock([su1, su2, su3], \"number\")\nsu1 = ListItem(\"Swim in the Seine — open July & August 2026\", \"First time in 102 years\", null, null, { type: \"continue_conversation\", context: \"Tell me about swimming in the Seine\" })\nsu2 = ListItem(\"Open-air cinema at Parc de la Villette\", \"Free films under the stars\", null, null, { type: \"continue_conversation\", context: \"Tell me about the open-air cinema at Parc de la Villette\" })\nsu3 = ListItem(\"Bastille Day celebrations on July 14\", \"Fireworks at the Eiffel Tower\", null, null, { type: \"continue_conversation\", context: \"What happens in Paris on Bastille Day?\" })\nactionBtns = Buttons([btn1, btn2, btn3], \"row\")\nbtn1 = Button(\"Plan a Trip to Paris\", { type: \"continue_conversation\", context: \"Help me plan a trip to Paris\" }, \"primary\")\nbtn2 = Button(\"Best Hotels in Paris\", { type: \"continue_conversation\", context: \"Show me the best hotels in Paris\" }, \"secondary\")\nbtn3 = Button(\"Build a Paris Itinerary\", { type: \"continue_conversation\", context: \"Build a 5-day Paris itinerary for me\" }, \"secondary\")\nsrc1 = { title: \"Paris Travel Guide 2025\", sourceName: \"Travel and Tour World\", url: \"https://www.travelandtourworld.com/news/article/from-iconic-landmarks-to-hidden-gems-the-ultimate-paris-travel-guide-for-2025-you-need-to-read-now/\" }\nsrc2 = { title: \"Best Things to Do in Paris 2026\", sourceName: \"Time Out Paris\", url: \"http://www.timeout.fr/paris/en/for-tourists\" }"} +] diff --git a/packages/swift-lang/Tests/OpenUILangTests/Fixtures/messages.json b/packages/swift-lang/Tests/OpenUILangTests/Fixtures/messages.json new file mode 100644 index 0000000000..15df201b09 --- /dev/null +++ b/packages/swift-lang/Tests/OpenUILangTests/Fixtures/messages.json @@ -0,0 +1,6 @@ +{ +"separate": [{"name":"empty","input":"","expected":{"content":"","contextString":null}},{"name":"plain text","input":"Just some text","expected":{"content":"Just some text","contextString":null}},{"name":"content only","input":"]]>openui:content\nroot = Card([])","expected":{"content":"root = Card([])","contextString":null,"contentHeader":"]]>openui:content"}},{"name":"content with header attrs","input":"]]>openui:content?thesys=true&libraryVersion=0.1.0\nroot = Card([])","expected":{"content":"root = Card([])","contextString":null,"contentHeader":"]]>openui:content?thesys=true&libraryVersion=0.1.0"}},{"name":"content and context","input":"]]>openui:content\nroot = Card([])\n]]>openui:context\n[{\"f\":1}]","expected":{"content":"root = Card([])","contextString":"[{\"f\":1}]","contentHeader":"]]>openui:content"}},{"name":"context only","input":"Hello\n]]>openui:context\n[\"User clicked: Go\"]","expected":{"content":"Hello","contextString":"[\"User clicked: Go\"]"}},{"name":"context then content","input":"]]>openui:context\n[1]\n]]>openui:content\nroot = X()","expected":{"content":"root = X()","contextString":null,"contentHeader":"]]>openui:content"}},{"name":"last content wins","input":"]]>openui:content\nold\n]]>openui:content\nnew","expected":{"content":"new","contextString":null,"contentHeader":"]]>openui:content"}},{"name":"crlf separators","input":"]]>openui:content\r\nroot = X()\r\n]]>openui:context\r\n[1]","expected":{"content":"root = X()","contextString":"[1]","contentHeader":"]]>openui:content\r"}},{"name":"end marker","input":"]]>openui:content\nroot = X()\n]]>openui:end","expected":{"content":"root = X()","contextString":null,"contentHeader":"]]>openui:content","end":true}},{"name":"end marker with attrs","input":"]]>openui:content\nroot = X()\n]]>openui:end?status=done","expected":{"content":"root = X()","contextString":null,"contentHeader":"]]>openui:content","end":true}},{"name":"text after end marker","input":"]]>openui:content\nroot = X()\n]]>openui:end\ntrailing","expected":{"content":"root = X()\ntrailing","contextString":null,"contentHeader":"]]>openui:content","end":true}},{"name":"end between sections","input":"]]>openui:content\na\n]]>openui:end\n]]>openui:context\n[1]","expected":{"content":"a","contextString":"[1]","contentHeader":"]]>openui:content","end":true}},{"name":"two end markers","input":"]]>openui:content\na\n]]>openui:end\n]]>openui:end","expected":{"content":"a","contextString":null,"contentHeader":"]]>openui:content","end":true}},{"name":"crlf before end","input":"]]>openui:content\na\r\n]]>openui:end","expected":{"content":"a","contextString":null,"contentHeader":"]]>openui:content","end":true}},{"name":"partial tail one char","input":"]]>openui:content\nroot = X()\n]","expected":{"content":"root = X()","contextString":null,"contentHeader":"]]>openui:content"}},{"name":"partial tail sentinel","input":"]]>openui:content\nroot = X()\n]]>openui:","expected":{"content":"root = X()","contextString":null,"contentHeader":"]]>openui:content"}},{"name":"partial context marker","input":"]]>openui:content\nroot = X()\n]]>openui:cont","expected":{"content":"root = X()","contextString":null,"contentHeader":"]]>openui:content"}},{"name":"partial end marker","input":"]]>openui:content\nroot = X()\n]]>openui:e","expected":{"content":"root = X()","contextString":null,"contentHeader":"]]>openui:content"}},{"name":"bracket that isn't a marker","input":"]]>openui:content\nx = [1, 2]","expected":{"content":"x = [1, 2","contextString":null,"contentHeader":"]]>openui:content"}},{"name":"marker header without newline","input":"]]>openui:content","expected":{"content":"","contextString":null,"contentHeader":"]]>openui:content"}},{"name":"markers on one line","input":"]]>openui:content]]>openui:context\n[1]","expected":{"content":"","contextString":"[1]","contentHeader":"]]>openui:content]]>openui:context"}},{"name":"emoji","input":"]]>openui:content\nroot = TextContent(\"🙂 hi\")\n]]>openui:context\n[\"🎉\"]","expected":{"content":"root = TextContent(\"🙂 hi\")","contextString":"[\"🎉\"]","contentHeader":"]]>openui:content"}},{"name":"legacy xml","input":"root = X()\n[{\"a\":1}]","expected":{"content":"root = X()","contextString":"[{\"a\":1}]"}},{"name":"legacy content only","input":"root = X() ","expected":{"content":"root = X()","contextString":null}},{"name":"legacy context only","input":"text\n[\"x\"]\n","expected":{"content":"text","contextString":"[\"x\"]"}},{"name":"legacy unclosed","input":"root = X()","expected":{"content":"root = X()","contextString":null}},{"name":"legacy text after context","input":"[1] more","expected":{"content":"[1] more","contextString":null}}], +"langSyntax": [{"input":"```openui-lang\nroot = X()\n```","expected":true},{"input":"root = X()","expected":true},{"input":"root=X()","expected":true},{"input":" root = X()","expected":true},{"input":"intro\n\troot = X()","expected":true},{"input":"intro\n\n root\n= X()","expected":true},{"input":"rooted = 1","expected":false},{"input":"x root = 1","expected":false},{"input":"Just text","expected":false},{"input":"","expected":false}], +"artifacts": [{"input":"]]>openui:artifact {\"artifact_id\":\"a1\",\"type\":\"slides\",\"name\":\"Deck\",\"version\":\"2\"}\nroot = X()","expected":{"header":{"artifact_id":"a1","type":"slides","name":"Deck","version":"2"},"program":"root = X()"}},{"input":"]]>openui:artifact {\"artifact_id\":\"a2\",\"type\":\"presentation\"}\nprogram","expected":{"header":{"artifact_id":"a2","type":"slides"},"program":"program"}},{"input":"]]>openui:artifact {\"artifact_id\":\"a3\",\"type\":\"report\",\"name\":\"\"}","expected":{"header":{"artifact_id":"a3","type":"report"},"program":""}},{"input":"]]>openui:artifact {\"artifact_id\":\"\",\"type\":\"report\"}\nx","expected":null},{"input":"]]>openui:artifact {\"type\":\"report\"}\nx","expected":null},{"input":"]]>openui:artifact {\"artifact_id\":\"a4\",\"type\":\"video\"}\nx","expected":null},{"input":"]]>openui:artifact {\"artifact_id\":\"a5\",\"type\":\"report\",\"version\":7}\nx","expected":{"header":{"artifact_id":"a5","type":"report"},"program":"x"}},{"input":"]]>openui:artifact not json\nx","expected":null},{"input":"]]>openui:artifact []\nx","expected":null},{"input":"]]>openui:artifact{\"artifact_id\":\"a6\",\"type\":\"report\"}\nx","expected":null},{"input":"]]>openui:content\nroot = X()","expected":null}], +"wrap": {"content":"]]>openui:content\nroot = X()","contentWithHeader":"]]>openui:content?thesys=true\nroot = X()","contentWithoutHeader":"]]>openui:content\nroot = X()","context":"\n]]>openui:context\n[{\"a\":1}]"} +} diff --git a/packages/swift-lang/Tests/OpenUILangTests/Fixtures/openui-spec.json b/packages/swift-lang/Tests/OpenUILangTests/Fixtures/openui-spec.json new file mode 100644 index 0000000000..d9b3e5e96c --- /dev/null +++ b/packages/swift-lang/Tests/OpenUILangTests/Fixtures/openui-spec.json @@ -0,0 +1,6 @@ +{ +"root": "Stack", +"components": {"Card":{"signature":"Card(children: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock | Tabs | Carousel | Stack)[], variant?: \"card\" | \"sunk\" | \"clear\", direction?: \"row\" | \"column\", gap?: \"none\" | \"xs\" | \"s\" | \"m\" | \"l\" | \"xl\" | \"2xl\", align?: \"start\" | \"center\" | \"end\" | \"stretch\" | \"baseline\", justify?: \"start\" | \"center\" | \"end\" | \"between\" | \"around\" | \"evenly\", wrap?: boolean)","description":"Styled container. variant: \"card\" (default, elevated) | \"sunk\" (recessed) | \"clear\" (transparent). Always full width. Accepts all Stack flex params (default: direction \"column\"). Cards flex to share space in row/wrap layouts."},"CardHeader":{"signature":"CardHeader(title?: string, subtitle?: string)","description":"Header with optional title and subtitle"},"TextContent":{"signature":"TextContent(text: string, size?: \"small\" | \"default\" | \"large\" | \"small-heavy\" | \"large-heavy\")","description":"Text block. Supports markdown. Optional size: \"small\" | \"default\" | \"large\" | \"small-heavy\" | \"large-heavy\"."},"MarkDownRenderer":{"signature":"MarkDownRenderer(textMarkdown: string, variant?: \"clear\" | \"card\" | \"sunk\")","description":"Renders markdown text with optional container variant"},"Callout":{"signature":"Callout(variant: \"info\" | \"warning\" | \"error\" | \"success\" | \"neutral\", title: string, description: string, visible?: $binding)","description":"Callout banner. Optional visible is a reactive $boolean — auto-dismisses after 3s by setting $visible to false."},"TextCallout":{"signature":"TextCallout(variant?: \"neutral\" | \"info\" | \"warning\" | \"success\" | \"danger\", title?: string, description?: string)","description":"Text callout with variant, title, and description"},"Image":{"signature":"Image(alt: string, src?: string)","description":"Image with alt text and optional URL"},"ImageBlock":{"signature":"ImageBlock(src: string, alt?: string)","description":"Image block with loading state"},"ImageGallery":{"signature":"ImageGallery(images: {src: string, alt?: string, details?: string}[])","description":"Gallery grid of images with modal preview"},"CodeBlock":{"signature":"CodeBlock(language: string, codeString: string)","description":"Syntax-highlighted code block"},"InlineHeader":{"signature":"InlineHeader(heading: string, description?: string)","description":"Compact section heading with an optional one-line description, for use inside cards."},"Table":{"signature":"Table(columns: Col[])","description":"Data table — column-oriented. Each Col holds its own data array."},"Col":{"signature":"Col(label: string, data: any, type?: \"string\" | \"number\" | \"action\")","description":"Column definition — holds label + data array"},"EditableTable":{"signature":"EditableTable(name?: string, columns?: {type: \"text\" | \"number\" | \"date-single\" | \"select\" | \"url\", key?: string, header?: string, width?: number, options?: {value: string, label: string}[]}[], data?: {id: string, values: (string | number)[]}[])","description":"Spreadsheet-like table whose cells the user can edit inline (text, number, url, date, select columns); edits are saved back as a form field"},"BarChart":{"signature":"BarChart(labels: string[], series: Series[], variant?: \"grouped\" | \"stacked\", xLabel?: string, yLabel?: string, height?: number)","description":"Vertical bars; use for comparing values across categories with one or more series"},"LineChart":{"signature":"LineChart(labels: string[], series: Series[], variant?: \"linear\" | \"natural\" | \"step\", xLabel?: string, yLabel?: string, height?: number)","description":"Lines over categories; use for trends and continuous data over time"},"AreaChart":{"signature":"AreaChart(labels: string[], series: Series[], variant?: \"linear\" | \"natural\" | \"step\", xLabel?: string, yLabel?: string, height?: number)","description":"Filled area under lines; use for cumulative totals or volume trends over time"},"RadarChart":{"signature":"RadarChart(labels: string[], series: Series[])","description":"Spider/web chart; use for comparing multiple variables across one or more entities"},"HorizontalBarChart":{"signature":"HorizontalBarChart(labels: string[], series: Series[], variant?: \"grouped\" | \"stacked\", xLabel?: string, yLabel?: string)","description":"Horizontal bars; prefer when category labels are long or for ranked lists"},"Series":{"signature":"Series(category: string, values: number[])","description":"One data series"},"PieChart":{"signature":"PieChart(labels: string[], values: number[], variant?: \"pie\" | \"donut\", appearance?: \"circular\" | \"semiCircular\")","description":"Circular slices; use plucked arrays: PieChart(data.categories, data.values)"},"RadialChart":{"signature":"RadialChart(labels: string[], values: number[])","description":"Radial bars; use plucked arrays: RadialChart(data.categories, data.values)"},"SingleStackedBarChart":{"signature":"SingleStackedBarChart(labels: string[], values: number[])","description":"Single horizontal stacked bar; use plucked arrays: SingleStackedBarChart(data.categories, data.values)"},"Slice":{"signature":"Slice(category: string, value: number)","description":"One slice with label and numeric value"},"ScatterChart":{"signature":"ScatterChart(datasets: ScatterSeries[], xLabel?: string, yLabel?: string)","description":"X/Y scatter plot; use for correlations, distributions, and clustering"},"ScatterSeries":{"signature":"ScatterSeries(name: string, points: Point[])","description":"Named dataset"},"Point":{"signature":"Point(x: number, y: number, z?: number)","description":"Data point with numeric coordinates"},"Form":{"signature":"Form(name: string, buttons: Buttons, fields?: FormControl[])","description":"Form container with fields and explicit action buttons"},"FormControl":{"signature":"FormControl(label: string, input: Input | TextArea | Select | DatePicker | Slider | CheckBoxGroup | RadioGroup | Chips | OptionCards, hint?: string)","description":"Field with label, input component, and optional hint text"},"Label":{"signature":"Label(text: string)","description":"Text label"},"Input":{"signature":"Input(name: string, placeholder?: string, type?: \"text\" | \"email\" | \"password\" | \"number\" | \"url\", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)","description":""},"TextArea":{"signature":"TextArea(name: string, placeholder?: string, rows?: number, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)","description":""},"Select":{"signature":"Select(name: string, items: SelectItem[], placeholder?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding, size?: \"small\" | \"medium\" | \"large\")","description":""},"SelectItem":{"signature":"SelectItem(value: string, label: string)","description":"Option for Select"},"DatePicker":{"signature":"DatePicker(name: string, mode?: \"single\" | \"range\", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)","description":""},"Slider":{"signature":"Slider(name: string, variant: \"continuous\" | \"discrete\", min: number, max: number, step?: number, defaultValue?: number[], label?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)","description":"Numeric slider input; supports continuous and discrete (stepped) variants"},"CheckBoxGroup":{"signature":"CheckBoxGroup(name: string, items: CheckBoxItem[], rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding>)","description":""},"CheckBoxItem":{"signature":"CheckBoxItem(label: string, description: string, name: string, defaultChecked?: boolean)","description":""},"RadioGroup":{"signature":"RadioGroup(name: string, items: RadioItem[], defaultValue?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)","description":""},"RadioItem":{"signature":"RadioItem(label: string, description: string, value: string)","description":""},"SwitchGroup":{"signature":"SwitchGroup(name: string, items: SwitchItem[], variant?: \"clear\" | \"card\" | \"sunk\", value?: $binding>)","description":"Group of switch toggles"},"SwitchItem":{"signature":"SwitchItem(label?: string, description?: string, name: string, defaultChecked?: boolean)","description":"Individual switch toggle"},"ChipItem":{"signature":"ChipItem(value: string, label: string, icon?: Icon, disabled?: boolean)","description":"A single selectable chip inside a Chips group, with a value, label and optional icon."},"Chips":{"signature":"Chips(name: string, type?: \"single\" | \"multiple\", items?: ChipItem[], rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, defaultValue?: string | string[])","description":"A form field of compact selectable chips for choosing one or many short options; the selection is stored under `name`."},"OptionCard":{"signature":"OptionCard(value: string, title: string, subtitle?: string, topContent?: Icon | Image, disabled?: boolean)","description":"A single selectable card inside an OptionCards group, with a value, title, optional subtitle and an optional Icon or Image on top."},"OptionCards":{"signature":"OptionCards(name: string, type?: \"single\" | \"multiple\", items?: OptionCard[], rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, defaultValue?: string | string[])","description":"A form field of selectable cards (title, optional subtitle, optional icon or image) laid out in a responsive grid for choosing one or many options; the selection is stored under `name`."},"Button":{"signature":"Button(label: string, action?: ActionExpression, variant?: \"primary\" | \"secondary\" | \"tertiary\", type?: \"normal\" | \"destructive\", size?: \"extra-small\" | \"small\" | \"medium\" | \"large\")","description":"Clickable button"},"Buttons":{"signature":"Buttons(buttons: Button[], direction?: \"row\" | \"column\")","description":"Group of Button components. direction: \"row\" (default) | \"column\"."},"IconButton":{"signature":"IconButton(name: string, icon: Icon, action?: ActionExpression, variant?: \"primary\" | \"secondary\" | \"tertiary\", size?: \"extra-small\" | \"small\" | \"medium\" | \"large\", shape?: \"square\" | \"circle\")","description":"Icon-only button. name is the accessible label and the action label; icon is an Icon; action fires on click."},"Stack":{"signature":"Stack(children: any[], direction?: \"row\" | \"column\", gap?: \"none\" | \"xs\" | \"s\" | \"m\" | \"l\" | \"xl\" | \"2xl\", align?: \"start\" | \"center\" | \"end\" | \"stretch\" | \"baseline\", justify?: \"start\" | \"center\" | \"end\" | \"between\" | \"around\" | \"evenly\", wrap?: boolean)","description":"Flex container. direction: \"row\"|\"column\" (default \"column\"). gap: \"none\"|\"xs\"|\"s\"|\"m\"|\"l\"|\"xl\"|\"2xl\" (default \"m\"). align: \"start\"|\"center\"|\"end\"|\"stretch\"|\"baseline\". justify: \"start\"|\"center\"|\"end\"|\"between\"|\"around\"|\"evenly\"."},"Tabs":{"signature":"Tabs(items: TabItem[])","description":"Tabbed container"},"TabItem":{"signature":"TabItem(value: string, trigger: string, content: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock)[])","description":"value is unique id, trigger is tab label, content is array of components"},"Accordion":{"signature":"Accordion(items: AccordionItem[])","description":"Collapsible sections"},"AccordionItem":{"signature":"AccordionItem(value: string, trigger: string, content: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock)[])","description":"value is unique id, trigger is section title"},"Steps":{"signature":"Steps(items: StepsItem[])","description":"Step-by-step guide"},"StepsItem":{"signature":"StepsItem(title: string, details: string)","description":"title and details text for one step"},"Carousel":{"signature":"Carousel(children: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock)[][], variant?: \"card\" | \"sunk\")","description":"Horizontal scrollable carousel"},"Separator":{"signature":"Separator(orientation?: \"horizontal\" | \"vertical\", decorative?: boolean)","description":"Visual divider between content sections"},"TagBlock":{"signature":"TagBlock(tags: string[], size?: \"sm\" | \"md\" | \"lg\")","description":"tags is an array of strings; optional size sm | md | lg"},"Tag":{"signature":"Tag(text: string, icon?: Icon, size?: \"sm\" | \"md\" | \"lg\", variant?: \"neutral\" | \"info\" | \"success\" | \"warning\" | \"danger\")","description":"Styled tag/badge with optional Icon and variant"},"Icon":{"signature":"Icon(name: string, category?: string)","description":"A lucide icon by kebab-case name (e.g. 'circle-check'). Optional category picks a topical fallback when the name doesn't resolve."},"EntityList":{"signature":"EntityList(rows?: {left: string, right: string, rightVariant?: \"text\" | \"number\"}[], size?: \"small\" | \"default\", header?: {left: string, right: string, rightVariant?: \"text\" | \"number\"}, footer?: {left: string, right: string, rightVariant?: \"text\" | \"number\"})","description":"Two-column key/value rows (left label, right value). size 'default' supports optional header and footer rows; rightVariant 'number' uses tabular numbers."},"ListBlock":{"signature":"ListBlock(items: ListItem[], variant?: \"number\" | \"image\", size?: \"default\" | \"small\")","description":"A list of items with number or image indicators. Each item can optionally have an action. size small renders a compact list."},"ListItem":{"signature":"ListItem(title: string, subtitle?: string, image?: {src: string, alt: string}, actionLabel?: string, action?: ActionExpression)","description":"Item in a ListBlock — displays a title with an optional subtitle and image. When action is provided, the item becomes clickable."},"Text":{"signature":"Text(variant?: \"text\" | \"number\", value: string, subtext?: string, subtextVariant?: \"text\" | \"number\" | \"metric\", size?: \"xs\" | \"sm\" | \"md\" | \"lg\")","description":"Plain text line with optional subtext. variant 'number' uses tabular number styling; subtextVariant 'metric' colors a leading +/- subtext green/red."},"BoldText":{"signature":"BoldText(variant?: \"text\" | \"number\", value: string, subtext?: string, subtextVariant?: \"text\" | \"number\" | \"metric\", size?: \"xs\" | \"sm\" | \"md\" | \"lg\")","description":"Emphasized (bold) text line with optional subtext. variant 'number' uses tabular number styling; subtextVariant 'metric' colors a leading +/- subtext green/red."},"IconText":{"signature":"IconText(icon: Icon, iconVariant?: \"neutral\" | \"info\" | \"success\" | \"warning\" | \"danger\" | \"inverted\" | \"filled\" | \"soft\", iconSize?: \"xs\" | \"s\" | \"m\" | \"l\" | \"xl\" | \"sm\" | \"md\" | \"lg\", title: string, subtitle?: string, bold?: boolean, layout?: \"horizontal\" | \"vertical\")","description":"An icon badge with a title and optional subtitle, laid out horizontally or vertically. iconVariant sets the badge color."},"ImageText":{"signature":"ImageText(src: string, alt?: string, title: string, subtitle?: string, bold?: boolean, layout?: \"horizontal\" | \"vertical\", imageSize?: number)","description":"A small square image (thumbnail/avatar) with a title and optional subtitle. src must be a real image URL."},"ImageTextLarge":{"signature":"ImageTextLarge(src: string, alt?: string, title: string, subtitle?: string, bold?: boolean)","description":"A full-width banner image above a bold title and optional subtitle. src must be a real image URL."},"MetricIndicatorInline":{"signature":"MetricIndicatorInline(value: string, subtext?: string, trend?: {direction: \"up\" | \"down\", value: number})","description":"Headline metric value with an optional +/- percentage trend and subtext, all on one line."},"MetricIndicatorWithStrikethrough":{"signature":"MetricIndicatorWithStrikethrough(value: string, subtext?: string, previousValue?: string, trend?: {direction: \"up\" | \"down\", value: number})","description":"Headline metric value with an optional struck-through previousValue, a +/- percentage trend, and subtext below."},"SnippetCardItem":{"signature":"SnippetCardItem(id?: string, lhs: IconText | ImageText, rhs?: Text | BoldText)","description":"One row-style snippet card: a label on the left (IconText or ImageText) and an optional value on the right (Text or BoldText)."},"SnippetCardBlock":{"signature":"SnippetCardBlock(items: SnippetCardItem[], layout?: \"grid\", responsive?: boolean, action?: ActionExpression, gap?: number | string)","description":"A responsive grid of compact label/value cards (2 per row) for showing several short facts side by side; optionally clickable with a shared action."},"OverviewCardItem":{"signature":"OverviewCardItem(id?: string, top: IconText | ImageText | Text, bottom?: MetricIndicatorInline)","description":"One overview card: a heading slot at the top (IconText, ImageText or Text) and an optional MetricIndicatorInline at the bottom."},"OverviewCardBlock":{"signature":"OverviewCardBlock(items: OverviewCardItem[], layout?: \"grid\" | \"carousel\", responsive?: boolean, action?: ActionExpression, gap?: number | string)","description":"A grid or horizontal carousel of compact overview cards, each with a heading (icon/image/text) on top and an inline metric below; optionally clickable with a shared action."},"ContextCardItem":{"signature":"ContextCardItem(id?: string, title: string | Tag, body?: string, bgColor?: \"gray\", bgImageSrc?: string, bgImageAlt?: string)","description":"A single card inside a ContextCardBlock: a title (plain string or Tag), an optional markdown body, and an optional gray tint or background image."},"ContextCardBlock":{"signature":"ContextCardBlock(items: ContextCardItem[], layout?: \"grid\" | \"carousel\", responsive?: boolean, action?: ActionExpression, gap?: number | string)","description":"A grid or carousel of compact tinted context cards (title or tag plus a short bold body); an optional action makes every card clickable."},"CompositeCardItem":{"signature":"CompositeCardItem(id?: string, header?: IconText | ImageText | ImageTextLarge | Text | Image, body?: (Text | BoldText | MetricIndicatorInline | IconText | Image | AreaChart | BarChart | LineChart | ListBlock | TagBlock | EntityList)[], footer?: {price?: BoldText | MetricIndicatorWithStrikethrough, button?: Button})","description":"A single card inside a CompositeCardBlock: an optional header (icon/image/text), a stack of body elements (text, metrics, charts, lists, tags), and an optional price/button footer."},"CompositeCardBlock":{"signature":"CompositeCardBlock(items: CompositeCardItem[], layout?: \"grid\" | \"carousel\", responsive?: boolean, action?: ActionExpression, gap?: number | string)","description":"A two-per-row grid or carousel of rich cards, each with an optional header, stacked body content (text, metrics, charts, lists, tags) and a price/button footer; an optional action makes every card clickable."},"VisualCardItem":{"signature":"VisualCardItem(body: BoldText, id?: string, bgImageSrc?: string, tag?: Tag, bgImageAlt?: string)","description":"A single photo-first card inside a VisualCardBlock: a BoldText body panel, an optional Tag, and a background image (bgImageSrc must be a real URL; bgImageAlt is its alt text)."},"VisualCardBlock":{"signature":"VisualCardBlock(items: VisualCardItem[], layout?: \"grid\" | \"carousel\", responsive?: boolean, action?: ActionExpression, gap?: number | string)","description":"A grid or carousel of photo-first cards: a full-bleed background image with a tag on top and a bold text panel at the bottom; an optional action makes every card clickable."},"Modal":{"signature":"Modal(title: string, open?: $binding, children: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock)[], size?: \"sm\" | \"md\" | \"lg\")","description":"Modal dialog. open is a reactive $boolean binding — set to true to open, X/Escape/backdrop auto-closes. Put Form with buttons inside children."}}, +"componentGroups": [{"name":"Layout","components":["Stack","Tabs","TabItem","Accordion","AccordionItem","Steps","StepsItem","Carousel","Separator","Modal"],"notes":["- For grid-like layouts, use Stack with direction \"row\" and wrap set to true.","- Prefer justify \"start\" (or omit justify) with wrap=true for stable columns instead of uneven gutters.","- Use nested Stacks when you need explicit rows/sections.","- Show/hide sections: $editId != \"\" ? Card([editForm]) : null","- Modal: Modal(\"Title\", $showModal, [content]) — $showModal is boolean, X/Escape auto-closes. Put Form with its own buttons inside children.","- Use Tabs for alternative views (chart types, data sections) — no $variable needed","- Shared filter across Tabs: same $days binding in Query args works across all TabItems"]},{"name":"Content","components":["Card","CardHeader","TextContent","MarkDownRenderer","Callout","TextCallout","Image","ImageBlock","ImageGallery","CodeBlock","InlineHeader"],"notes":["- InlineHeader is a compact heading + description pair for labelling a block (lighter than CardHeader).","- Use Cards to group related KPIs or sections. Stack with direction \"row\" for side-by-side layouts.","- Success toast: Callout(\"success\", \"Saved\", \"Done.\", $showSuccess) — use @Set($showSuccess, true) in save action, auto-dismisses after 3s. For errors: result.status == \"error\" ? Callout(\"error\", \"Failed\", result.error) : null","- KPI card: Card([TextContent(\"Label\", \"small\"), TextContent(\"\" + @Count(@Filter(data.rows, \"field\", \"==\", \"value\")), \"large-heavy\")])"]},{"name":"Tables","components":["Table","Col","EditableTable"],"notes":["- EditableTable lets the user edit cells inline. Give it a unique name, columns of { type, key, header } with type one of text | number | date-single | select | url (select also needs options: [{ value, label }]).","- data is an array of { id, values } rows where values are ordered positionally to match columns. Edited data is submitted when the user clicks Save Changes.","- Table is COLUMN-oriented: Table([Col(\"Label\", dataArray), Col(\"Count\", countArray, \"number\")]). Use array pluck for data: data.rows.fieldName","- Col data can be component arrays for styled cells: Col(\"Status\", @Each(data.rows, \"item\", Tag(item.status, null, \"sm\", item.status == \"open\" ? \"success\" : \"danger\")))","- Row actions: Col(\"Actions\", @Each(data.rows, \"t\", Button(\"Edit\", Action([@Set($showEdit, true), @Set($editId, t.id)]))))","- Sortable: sorted = @Sort(data.rows, $sortField, \"desc\"). Bind $sortField to Select. Use sorted.fieldName for Col data","- Searchable: filtered = @Filter(data.rows, \"title\", \"contains\", $search). Bind $search to Input","- Chain sort + filter: filtered = @Filter(...) then sorted = @Sort(filtered, ...) — use sorted for both Table and Charts","- Empty state: @Count(data.rows) > 0 ? Table([...]) : TextContent(\"No data yet\")"]},{"name":"Charts (2D)","components":["BarChart","LineChart","AreaChart","RadarChart","HorizontalBarChart","Series"],"notes":["- Charts accept column arrays: LineChart(labels, [Series(\"Name\", values)]). Use array pluck: LineChart(data.rows.day, [Series(\"Views\", data.rows.views)])","- Use Cards to wrap charts with CardHeader for titled sections","- Chart + Table from same source: use @Sort or @Filter result for both LineChart and Table Col data","- Multiple chart views: use Tabs — Tabs([TabItem(\"line\", \"Line\", [LineChart(...)]), TabItem(\"bar\", \"Bar\", [BarChart(...)])])"]},{"name":"Charts (1D)","components":["PieChart","RadialChart","SingleStackedBarChart","Slice"],"notes":["- PieChart and BarChart need NUMBERS, not objects. For list data, use @Count(@Filter(...)) to aggregate:","- PieChart from list: `PieChart([\"Low\", \"Med\", \"High\"], [@Count(@Filter(data.rows, \"priority\", \"==\", \"low\")), @Count(@Filter(data.rows, \"priority\", \"==\", \"medium\")), @Count(@Filter(data.rows, \"priority\", \"==\", \"high\"))], \"donut\")`","- KPI from count: `TextContent(\"\" + @Count(@Filter(data.rows, \"status\", \"==\", \"open\")), \"large-heavy\")`"]},{"name":"Charts (Scatter)","components":["ScatterChart","ScatterSeries","Point"]},{"name":"Forms","components":["Form","FormControl","Label","Input","TextArea","Select","SelectItem","DatePicker","Slider","CheckBoxGroup","CheckBoxItem","RadioGroup","RadioItem","SwitchGroup","SwitchItem","Chips","ChipItem","OptionCards","OptionCard"],"notes":["- Chips: compact single/multiple selection pills. Use ChipItem references for each option.","- OptionCards: larger selectable cards with title, subtitle and an optional Icon or Image on top. Use OptionCard references for each option.","- For Form fields, define EACH FormControl as its own reference — do NOT inline all controls in one array. This allows progressive field-by-field streaming.","- NEVER nest Form inside Form — each Form should be a standalone container.","- Form requires explicit buttons. Always pass a Buttons(...) reference as the third Form argument.","- rules is an optional object: {required: true, email: true, minLength: 8, maxLength: 100}","- Available rules: required, email, min, max, minLength, maxLength, pattern, url, numeric","- The renderer shows error messages automatically — do NOT generate error text in the UI","- Conditional fields: $country == \"US\" ? stateField : $country == \"UK\" ? postcodeField : addressField","- Edit form in Modal: Modal(\"Edit\", $showEdit, [Form(\"edit\", Buttons([saveBtn, cancelBtn]), [fields...])]). Save button should include @Set($showEdit, false) to close modal."]},{"name":"Buttons","components":["Button","Buttons","IconButton"],"notes":["- Icon renders a lucide icon by kebab-case name; it is also used as the icon of IconButton, IconText and OptionCard.","- Toggle in @Each: @Each(rows, \"t\", Button(t.status == \"open\" ? \"Close\" : \"Reopen\", Action([...])))"]},{"name":"Data Display","components":["TagBlock","Tag","Icon","EntityList","ListBlock","ListItem"],"notes":["- Color-mapped Tag: Tag(value, null, \"sm\", value == \"high\" ? \"danger\" : value == \"medium\" ? \"warning\" : \"neutral\")","- EntityList is a compact two-column list of { left, right } rows (e.g. name / value). size='default' also supports a header and footer row; size='small' does not.","- ListBlock is a numbered or image list of ListItem references. An action on ListItem is optional."]},{"name":"Cards","components":["SnippetCardBlock","SnippetCardItem","OverviewCardBlock","OverviewCardItem","ContextCardBlock","ContextCardItem","CompositeCardBlock","CompositeCardItem","VisualCardBlock","VisualCardItem","Text","BoldText","IconText","ImageText","ImageTextLarge","MetricIndicatorInline","MetricIndicatorWithStrikethrough"],"notes":["- Card blocks lay out 2+ items in a responsive grid (or carousel where supported). Every item in a block must have the same structure.","- SnippetCardItem: small card with lhs (IconText | ImageText) and optional rhs (Text | BoldText) — good for key/value facts.","- OverviewCardItem: small card with top (IconText | ImageText | Text) and optional bottom MetricIndicatorInline — good for KPIs.","- ContextCardItem: medium card with a title (string or Tag), body text and optional background image — good for summaries.","- CompositeCardItem: rich card with header, body array (Text, BoldText, MetricIndicatorInline, IconText, Image, charts, ListBlock, TagBlock, EntityList) and footer (price + Button) — good for products/offers.","- VisualCardItem: image-first card with a BoldText body and optional Tag.","- Text / BoldText / IconText / ImageText / ImageTextLarge / MetricIndicator* are the inline building blocks used INSIDE card items; do not place them directly in the root Card."]}], +"schema": {"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"Card":{"$ref":"#/$defs/Card"},"CardHeader":{"$ref":"#/$defs/CardHeader"},"TextContent":{"$ref":"#/$defs/TextContent"},"MarkDownRenderer":{"$ref":"#/$defs/MarkDownRenderer"},"Callout":{"$ref":"#/$defs/Callout"},"TextCallout":{"$ref":"#/$defs/TextCallout"},"Image":{"$ref":"#/$defs/Image"},"ImageBlock":{"$ref":"#/$defs/ImageBlock"},"ImageGallery":{"$ref":"#/$defs/ImageGallery"},"CodeBlock":{"$ref":"#/$defs/CodeBlock"},"InlineHeader":{"$ref":"#/$defs/InlineHeader"},"Table":{"$ref":"#/$defs/Table"},"Col":{"$ref":"#/$defs/Col"},"EditableTable":{"$ref":"#/$defs/EditableTable"},"BarChart":{"$ref":"#/$defs/BarChart"},"LineChart":{"$ref":"#/$defs/LineChart"},"AreaChart":{"$ref":"#/$defs/AreaChart"},"RadarChart":{"$ref":"#/$defs/RadarChart"},"HorizontalBarChart":{"$ref":"#/$defs/HorizontalBarChart"},"Series":{"$ref":"#/$defs/Series"},"PieChart":{"$ref":"#/$defs/PieChart"},"RadialChart":{"$ref":"#/$defs/RadialChart"},"SingleStackedBarChart":{"$ref":"#/$defs/SingleStackedBarChart"},"Slice":{"$ref":"#/$defs/Slice"},"ScatterChart":{"$ref":"#/$defs/ScatterChart"},"ScatterSeries":{"$ref":"#/$defs/ScatterSeries"},"Point":{"$ref":"#/$defs/Point"},"Form":{"$ref":"#/$defs/Form"},"FormControl":{"$ref":"#/$defs/FormControl"},"Label":{"$ref":"#/$defs/Label"},"Input":{"$ref":"#/$defs/Input"},"TextArea":{"$ref":"#/$defs/TextArea"},"Select":{"$ref":"#/$defs/Select"},"SelectItem":{"$ref":"#/$defs/SelectItem"},"DatePicker":{"$ref":"#/$defs/DatePicker"},"Slider":{"$ref":"#/$defs/Slider"},"CheckBoxGroup":{"$ref":"#/$defs/CheckBoxGroup"},"CheckBoxItem":{"$ref":"#/$defs/CheckBoxItem"},"RadioGroup":{"$ref":"#/$defs/RadioGroup"},"RadioItem":{"$ref":"#/$defs/RadioItem"},"SwitchGroup":{"$ref":"#/$defs/SwitchGroup"},"SwitchItem":{"$ref":"#/$defs/SwitchItem"},"ChipItem":{"$ref":"#/$defs/ChipItem"},"Chips":{"$ref":"#/$defs/Chips"},"OptionCard":{"$ref":"#/$defs/OptionCard"},"OptionCards":{"$ref":"#/$defs/OptionCards"},"Button":{"$ref":"#/$defs/Button"},"Buttons":{"$ref":"#/$defs/Buttons"},"IconButton":{"$ref":"#/$defs/IconButton"},"Stack":{"$ref":"#/$defs/Stack"},"Tabs":{"$ref":"#/$defs/Tabs"},"TabItem":{"$ref":"#/$defs/TabItem"},"Accordion":{"$ref":"#/$defs/Accordion"},"AccordionItem":{"$ref":"#/$defs/AccordionItem"},"Steps":{"$ref":"#/$defs/Steps"},"StepsItem":{"$ref":"#/$defs/StepsItem"},"Carousel":{"$ref":"#/$defs/Carousel"},"Separator":{"$ref":"#/$defs/Separator"},"TagBlock":{"$ref":"#/$defs/TagBlock"},"Tag":{"$ref":"#/$defs/Tag"},"Icon":{"$ref":"#/$defs/Icon"},"EntityList":{"$ref":"#/$defs/EntityList"},"ListBlock":{"$ref":"#/$defs/ListBlock"},"ListItem":{"$ref":"#/$defs/ListItem"},"Text":{"$ref":"#/$defs/Text"},"BoldText":{"$ref":"#/$defs/BoldText"},"IconText":{"$ref":"#/$defs/IconText"},"ImageText":{"$ref":"#/$defs/ImageText"},"ImageTextLarge":{"$ref":"#/$defs/ImageTextLarge"},"MetricIndicatorInline":{"$ref":"#/$defs/MetricIndicatorInline"},"MetricIndicatorWithStrikethrough":{"$ref":"#/$defs/MetricIndicatorWithStrikethrough"},"SnippetCardItem":{"$ref":"#/$defs/SnippetCardItem"},"SnippetCardBlock":{"$ref":"#/$defs/SnippetCardBlock"},"OverviewCardItem":{"$ref":"#/$defs/OverviewCardItem"},"OverviewCardBlock":{"$ref":"#/$defs/OverviewCardBlock"},"ContextCardItem":{"$ref":"#/$defs/ContextCardItem"},"ContextCardBlock":{"$ref":"#/$defs/ContextCardBlock"},"CompositeCardItem":{"$ref":"#/$defs/CompositeCardItem"},"CompositeCardBlock":{"$ref":"#/$defs/CompositeCardBlock"},"VisualCardItem":{"$ref":"#/$defs/VisualCardItem"},"VisualCardBlock":{"$ref":"#/$defs/VisualCardBlock"},"Modal":{"$ref":"#/$defs/Modal"}},"required":["Card","CardHeader","TextContent","MarkDownRenderer","Callout","TextCallout","Image","ImageBlock","ImageGallery","CodeBlock","InlineHeader","Table","Col","EditableTable","BarChart","LineChart","AreaChart","RadarChart","HorizontalBarChart","Series","PieChart","RadialChart","SingleStackedBarChart","Slice","ScatterChart","ScatterSeries","Point","Form","FormControl","Label","Input","TextArea","Select","SelectItem","DatePicker","Slider","CheckBoxGroup","CheckBoxItem","RadioGroup","RadioItem","SwitchGroup","SwitchItem","ChipItem","Chips","OptionCard","OptionCards","Button","Buttons","IconButton","Stack","Tabs","TabItem","Accordion","AccordionItem","Steps","StepsItem","Carousel","Separator","TagBlock","Tag","Icon","EntityList","ListBlock","ListItem","Text","BoldText","IconText","ImageText","ImageTextLarge","MetricIndicatorInline","MetricIndicatorWithStrikethrough","SnippetCardItem","SnippetCardBlock","OverviewCardItem","OverviewCardBlock","ContextCardItem","ContextCardBlock","CompositeCardItem","CompositeCardBlock","VisualCardItem","VisualCardBlock","Modal"],"additionalProperties":false,"$defs":{"Card":{"type":"object","properties":{"children":{"type":"array","items":{"anyOf":[{"$ref":"#/$defs/TextContent"},{"$ref":"#/$defs/MarkDownRenderer"},{"$ref":"#/$defs/CardHeader"},{"$ref":"#/$defs/Callout"},{"$ref":"#/$defs/TextCallout"},{"$ref":"#/$defs/CodeBlock"},{"$ref":"#/$defs/Image"},{"$ref":"#/$defs/ImageBlock"},{"$ref":"#/$defs/ImageGallery"},{"$ref":"#/$defs/Separator"},{"$ref":"#/$defs/HorizontalBarChart"},{"$ref":"#/$defs/RadarChart"},{"$ref":"#/$defs/PieChart"},{"$ref":"#/$defs/RadialChart"},{"$ref":"#/$defs/SingleStackedBarChart"},{"$ref":"#/$defs/ScatterChart"},{"$ref":"#/$defs/AreaChart"},{"$ref":"#/$defs/BarChart"},{"$ref":"#/$defs/LineChart"},{"$ref":"#/$defs/Table"},{"$ref":"#/$defs/TagBlock"},{"$ref":"#/$defs/Form"},{"$ref":"#/$defs/Buttons"},{"$ref":"#/$defs/IconButton"},{"$ref":"#/$defs/Steps"},{"$ref":"#/$defs/InlineHeader"},{"$ref":"#/$defs/EntityList"},{"$ref":"#/$defs/EditableTable"},{"$ref":"#/$defs/SnippetCardBlock"},{"$ref":"#/$defs/OverviewCardBlock"},{"$ref":"#/$defs/ContextCardBlock"},{"$ref":"#/$defs/CompositeCardBlock"},{"$ref":"#/$defs/VisualCardBlock"},{"$ref":"#/$defs/Tabs"},{"$ref":"#/$defs/Carousel"},{"$ref":"#/$defs/Stack"}]}},"variant":{"type":"string","enum":["card","sunk","clear"]},"direction":{"type":"string","enum":["row","column"]},"gap":{"type":"string","enum":["none","xs","s","m","l","xl","2xl"]},"align":{"type":"string","enum":["start","center","end","stretch","baseline"]},"justify":{"type":"string","enum":["start","center","end","between","around","evenly"]},"wrap":{"type":"boolean"}},"required":["children"],"additionalProperties":false,"description":"Styled container. variant: \"card\" (default, elevated) | \"sunk\" (recessed) | \"clear\" (transparent). Always full width. Accepts all Stack flex params (default: direction \"column\"). Cards flex to share space in row/wrap layouts."},"TextContent":{"type":"object","properties":{"text":{"type":"string"},"size":{"type":"string","enum":["small","default","large","small-heavy","large-heavy"]}},"required":["text"],"additionalProperties":false,"description":"Text block. Supports markdown. Optional size: \"small\" | \"default\" | \"large\" | \"small-heavy\" | \"large-heavy\"."},"MarkDownRenderer":{"type":"object","properties":{"textMarkdown":{"type":"string"},"variant":{"type":"string","enum":["clear","card","sunk"]}},"required":["textMarkdown"],"additionalProperties":false,"description":"Renders markdown text with optional container variant"},"CardHeader":{"type":"object","properties":{"title":{"type":"string"},"subtitle":{"type":"string"}},"additionalProperties":false,"description":"Header with optional title and subtitle"},"Callout":{"type":"object","properties":{"variant":{"type":"string","enum":["info","warning","error","success","neutral"]},"title":{"type":"string"},"description":{"type":"string"},"visible":{"type":"boolean"}},"required":["variant","title","description"],"additionalProperties":false,"description":"Callout banner. Optional visible is a reactive $boolean — auto-dismisses after 3s by setting $visible to false."},"TextCallout":{"type":"object","properties":{"variant":{"type":"string","enum":["neutral","info","warning","success","danger"]},"title":{"type":"string"},"description":{"type":"string"}},"additionalProperties":false,"description":"Text callout with variant, title, and description"},"CodeBlock":{"type":"object","properties":{"language":{"type":"string"},"codeString":{"type":"string"}},"required":["language","codeString"],"additionalProperties":false,"description":"Syntax-highlighted code block"},"Image":{"type":"object","properties":{"alt":{"type":"string"},"src":{"type":"string"}},"required":["alt"],"additionalProperties":false,"description":"Image with alt text and optional URL"},"ImageBlock":{"type":"object","properties":{"src":{"type":"string"},"alt":{"type":"string"}},"required":["src"],"additionalProperties":false,"description":"Image block with loading state"},"ImageGallery":{"type":"object","properties":{"images":{"type":"array","items":{"type":"object","properties":{"src":{"type":"string"},"alt":{"type":"string"},"details":{"type":"string"}},"required":["src"],"additionalProperties":false}}},"required":["images"],"additionalProperties":false,"description":"Gallery grid of images with modal preview"},"Separator":{"type":"object","properties":{"orientation":{"type":"string","enum":["horizontal","vertical"]},"decorative":{"type":"boolean"}},"additionalProperties":false,"description":"Visual divider between content sections"},"HorizontalBarChart":{"type":"object","properties":{"labels":{"type":"array","items":{"type":"string"}},"series":{"type":"array","items":{"$ref":"#/$defs/Series"}},"variant":{"type":"string","enum":["grouped","stacked"]},"xLabel":{"type":"string"},"yLabel":{"type":"string"}},"required":["labels","series"],"additionalProperties":false,"description":"Horizontal bars; prefer when category labels are long or for ranked lists"},"Series":{"type":"object","properties":{"category":{"type":"string"},"values":{"type":"array","items":{"type":"number"}}},"required":["category","values"],"additionalProperties":false,"description":"One data series"},"RadarChart":{"type":"object","properties":{"labels":{"type":"array","items":{"type":"string"}},"series":{"type":"array","items":{"$ref":"#/$defs/Series"}}},"required":["labels","series"],"additionalProperties":false,"description":"Spider/web chart; use for comparing multiple variables across one or more entities"},"PieChart":{"type":"object","properties":{"labels":{"type":"array","items":{"type":"string"}},"values":{"type":"array","items":{"type":"number"}},"variant":{"type":"string","enum":["pie","donut"]},"appearance":{"type":"string","enum":["circular","semiCircular"]}},"required":["labels","values"],"additionalProperties":false,"description":"Circular slices; use plucked arrays: PieChart(data.categories, data.values)"},"RadialChart":{"type":"object","properties":{"labels":{"type":"array","items":{"type":"string"}},"values":{"type":"array","items":{"type":"number"}}},"required":["labels","values"],"additionalProperties":false,"description":"Radial bars; use plucked arrays: RadialChart(data.categories, data.values)"},"SingleStackedBarChart":{"type":"object","properties":{"labels":{"type":"array","items":{"type":"string"}},"values":{"type":"array","items":{"type":"number"}}},"required":["labels","values"],"additionalProperties":false,"description":"Single horizontal stacked bar; use plucked arrays: SingleStackedBarChart(data.categories, data.values)"},"ScatterChart":{"type":"object","properties":{"datasets":{"type":"array","items":{"$ref":"#/$defs/ScatterSeries"}},"xLabel":{"type":"string"},"yLabel":{"type":"string"}},"required":["datasets"],"additionalProperties":false,"description":"X/Y scatter plot; use for correlations, distributions, and clustering"},"ScatterSeries":{"type":"object","properties":{"name":{"type":"string"},"points":{"type":"array","items":{"$ref":"#/$defs/Point"}}},"required":["name","points"],"additionalProperties":false,"description":"Named dataset"},"Point":{"type":"object","properties":{"x":{"type":"number"},"y":{"type":"number"},"z":{"type":"number"}},"required":["x","y"],"additionalProperties":false,"description":"Data point with numeric coordinates"},"AreaChart":{"type":"object","properties":{"labels":{"type":"array","items":{"type":"string"}},"series":{"type":"array","items":{"$ref":"#/$defs/Series"}},"variant":{"type":"string","enum":["linear","natural","step"]},"xLabel":{"type":"string"},"yLabel":{"type":"string"},"height":{"type":"number"}},"required":["labels","series"],"additionalProperties":false,"description":"Filled area under lines; use for cumulative totals or volume trends over time"},"BarChart":{"type":"object","properties":{"labels":{"type":"array","items":{"type":"string"}},"series":{"type":"array","items":{"$ref":"#/$defs/Series"}},"variant":{"type":"string","enum":["grouped","stacked"]},"xLabel":{"type":"string"},"yLabel":{"type":"string"},"height":{"type":"number"}},"required":["labels","series"],"additionalProperties":false,"description":"Vertical bars; use for comparing values across categories with one or more series"},"LineChart":{"type":"object","properties":{"labels":{"type":"array","items":{"type":"string"}},"series":{"type":"array","items":{"$ref":"#/$defs/Series"}},"variant":{"type":"string","enum":["linear","natural","step"]},"xLabel":{"type":"string"},"yLabel":{"type":"string"},"height":{"type":"number"}},"required":["labels","series"],"additionalProperties":false,"description":"Lines over categories; use for trends and continuous data over time"},"Table":{"type":"object","properties":{"columns":{"type":"array","items":{"$ref":"#/$defs/Col"}}},"required":["columns"],"additionalProperties":false,"description":"Data table — column-oriented. Each Col holds its own data array."},"Col":{"type":"object","properties":{"label":{"type":"string"},"data":{},"type":{"type":"string","enum":["string","number","action"]}},"required":["label","data"],"additionalProperties":false,"description":"Column definition — holds label + data array"},"TagBlock":{"type":"object","properties":{"tags":{"type":"array","items":{"type":"string"}},"size":{"type":"string","enum":["sm","md","lg"]}},"required":["tags"],"additionalProperties":false,"description":"tags is an array of strings; optional size sm | md | lg"},"Form":{"type":"object","properties":{"name":{"type":"string"},"buttons":{"$ref":"#/$defs/Buttons"},"fields":{"default":[],"type":"array","items":{"$ref":"#/$defs/FormControl"}}},"required":["name","buttons","fields"],"additionalProperties":false,"description":"Form container with fields and explicit action buttons"},"Buttons":{"type":"object","properties":{"buttons":{"type":"array","items":{"$ref":"#/$defs/Button"}},"direction":{"type":"string","enum":["row","column"]}},"required":["buttons"],"additionalProperties":false,"description":"Group of Button components. direction: \"row\" (default) | \"column\"."},"Button":{"type":"object","properties":{"label":{"type":"string"},"action":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","const":"open_url"},"url":{"type":"string"}},"required":["type","url"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string","const":"continue_conversation"},"context":{"type":"string"}},"required":["type"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string"},"params":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["type"],"additionalProperties":false}]},"variant":{"type":"string","enum":["primary","secondary","tertiary"]},"type":{"type":"string","enum":["normal","destructive"]},"size":{"type":"string","enum":["extra-small","small","medium","large"]}},"required":["label"],"additionalProperties":false,"description":"Clickable button"},"FormControl":{"type":"object","properties":{"label":{"type":"string"},"input":{"anyOf":[{"$ref":"#/$defs/Input"},{"$ref":"#/$defs/TextArea"},{"$ref":"#/$defs/Select"},{"$ref":"#/$defs/DatePicker"},{"$ref":"#/$defs/Slider"},{"$ref":"#/$defs/CheckBoxGroup"},{"$ref":"#/$defs/RadioGroup"},{"$ref":"#/$defs/Chips"},{"$ref":"#/$defs/OptionCards"}]},"hint":{"type":"string"}},"required":["label","input"],"additionalProperties":false,"description":"Field with label, input component, and optional hint text"},"Input":{"type":"object","properties":{"name":{"type":"string"},"placeholder":{"type":"string"},"type":{"type":"string","enum":["text","email","password","number","url"]},"rules":{"type":"object","properties":{"required":{"type":"boolean"},"email":{"type":"boolean"},"url":{"type":"boolean"},"numeric":{"type":"boolean"},"min":{"type":"number"},"max":{"type":"number"},"minLength":{"type":"number"},"maxLength":{"type":"number"},"pattern":{"type":"string"}},"additionalProperties":false},"value":{"type":"string"}},"required":["name"],"additionalProperties":false},"TextArea":{"type":"object","properties":{"name":{"type":"string"},"placeholder":{"type":"string"},"rows":{"type":"number"},"rules":{"type":"object","properties":{"required":{"type":"boolean"},"email":{"type":"boolean"},"url":{"type":"boolean"},"numeric":{"type":"boolean"},"min":{"type":"number"},"max":{"type":"number"},"minLength":{"type":"number"},"maxLength":{"type":"number"},"pattern":{"type":"string"}},"additionalProperties":false},"value":{"type":"string"}},"required":["name"],"additionalProperties":false},"Select":{"type":"object","properties":{"name":{"type":"string"},"items":{"type":"array","items":{"$ref":"#/$defs/SelectItem"}},"placeholder":{"type":"string"},"rules":{"type":"object","properties":{"required":{"type":"boolean"},"email":{"type":"boolean"},"url":{"type":"boolean"},"numeric":{"type":"boolean"},"min":{"type":"number"},"max":{"type":"number"},"minLength":{"type":"number"},"maxLength":{"type":"number"},"pattern":{"type":"string"}},"additionalProperties":false},"value":{"type":"string"},"size":{"type":"string","enum":["small","medium","large"]}},"required":["name","items"],"additionalProperties":false},"SelectItem":{"type":"object","properties":{"value":{"type":"string"},"label":{"type":"string"}},"required":["value","label"],"additionalProperties":false,"description":"Option for Select"},"DatePicker":{"type":"object","properties":{"name":{"type":"string"},"mode":{"type":"string","enum":["single","range"]},"rules":{"type":"object","properties":{"required":{"type":"boolean"},"email":{"type":"boolean"},"url":{"type":"boolean"},"numeric":{"type":"boolean"},"min":{"type":"number"},"max":{"type":"number"},"minLength":{"type":"number"},"maxLength":{"type":"number"},"pattern":{"type":"string"}},"additionalProperties":false},"value":{}},"required":["name"],"additionalProperties":false},"Slider":{"type":"object","properties":{"name":{"type":"string"},"variant":{"type":"string","enum":["continuous","discrete"]},"min":{"type":"number"},"max":{"type":"number"},"step":{"type":"number"},"defaultValue":{"type":"array","items":{"type":"number"}},"label":{"type":"string"},"rules":{"type":"object","properties":{"required":{"type":"boolean"},"email":{"type":"boolean"},"url":{"type":"boolean"},"numeric":{"type":"boolean"},"min":{"type":"number"},"max":{"type":"number"},"minLength":{"type":"number"},"maxLength":{"type":"number"},"pattern":{"type":"string"}},"additionalProperties":false},"value":{"type":"array","items":{"type":"number"}}},"required":["name","variant","min","max"],"additionalProperties":false,"description":"Numeric slider input; supports continuous and discrete (stepped) variants"},"CheckBoxGroup":{"type":"object","properties":{"name":{"type":"string"},"items":{"type":"array","items":{"$ref":"#/$defs/CheckBoxItem"}},"rules":{"type":"object","properties":{"required":{"type":"boolean"},"email":{"type":"boolean"},"url":{"type":"boolean"},"numeric":{"type":"boolean"},"min":{"type":"number"},"max":{"type":"number"},"minLength":{"type":"number"},"maxLength":{"type":"number"},"pattern":{"type":"string"}},"additionalProperties":false},"value":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"boolean"}}},"required":["name","items"],"additionalProperties":false},"CheckBoxItem":{"type":"object","properties":{"label":{"type":"string"},"description":{"type":"string"},"name":{"type":"string"},"defaultChecked":{"type":"boolean"}},"required":["label","description","name"],"additionalProperties":false},"RadioGroup":{"type":"object","properties":{"name":{"type":"string"},"items":{"type":"array","items":{"$ref":"#/$defs/RadioItem"}},"defaultValue":{"type":"string"},"rules":{"type":"object","properties":{"required":{"type":"boolean"},"email":{"type":"boolean"},"url":{"type":"boolean"},"numeric":{"type":"boolean"},"min":{"type":"number"},"max":{"type":"number"},"minLength":{"type":"number"},"maxLength":{"type":"number"},"pattern":{"type":"string"}},"additionalProperties":false},"value":{"type":"string"}},"required":["name","items"],"additionalProperties":false},"RadioItem":{"type":"object","properties":{"label":{"type":"string"},"description":{"type":"string"},"value":{"type":"string"}},"required":["label","description","value"],"additionalProperties":false},"Chips":{"type":"object","properties":{"name":{"type":"string"},"type":{"default":"multiple","type":"string","enum":["single","multiple"]},"items":{"default":[],"type":"array","items":{"$ref":"#/$defs/ChipItem"}},"rules":{"type":"object","properties":{"required":{"type":"boolean"},"email":{"type":"boolean"},"url":{"type":"boolean"},"numeric":{"type":"boolean"},"min":{"type":"number"},"max":{"type":"number"},"minLength":{"type":"number"},"maxLength":{"type":"number"},"pattern":{"type":"string"}},"additionalProperties":false},"defaultValue":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]}},"required":["name","type","items"],"additionalProperties":false,"description":"A form field of compact selectable chips for choosing one or many short options; the selection is stored under `name`."},"ChipItem":{"type":"object","properties":{"value":{"type":"string"},"label":{"type":"string"},"icon":{"$ref":"#/$defs/Icon"},"disabled":{"type":"boolean"}},"required":["value","label"],"additionalProperties":false,"description":"A single selectable chip inside a Chips group, with a value, label and optional icon."},"Icon":{"type":"object","properties":{"name":{"type":"string"},"category":{"type":"string"}},"required":["name"],"additionalProperties":false,"description":"A lucide icon by kebab-case name (e.g. 'circle-check'). Optional category picks a topical fallback when the name doesn't resolve."},"OptionCards":{"type":"object","properties":{"name":{"type":"string"},"type":{"default":"single","type":"string","enum":["single","multiple"]},"items":{"default":[],"type":"array","items":{"$ref":"#/$defs/OptionCard"}},"rules":{"type":"object","properties":{"required":{"type":"boolean"},"email":{"type":"boolean"},"url":{"type":"boolean"},"numeric":{"type":"boolean"},"min":{"type":"number"},"max":{"type":"number"},"minLength":{"type":"number"},"maxLength":{"type":"number"},"pattern":{"type":"string"}},"additionalProperties":false},"defaultValue":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]}},"required":["name","type","items"],"additionalProperties":false,"description":"A form field of selectable cards (title, optional subtitle, optional icon or image) laid out in a responsive grid for choosing one or many options; the selection is stored under `name`."},"OptionCard":{"type":"object","properties":{"value":{"type":"string"},"title":{"type":"string"},"subtitle":{"type":"string"},"topContent":{"anyOf":[{"$ref":"#/$defs/Icon"},{"$ref":"#/$defs/Image"}]},"disabled":{"type":"boolean"}},"required":["value","title"],"additionalProperties":false,"description":"A single selectable card inside an OptionCards group, with a value, title, optional subtitle and an optional Icon or Image on top."},"IconButton":{"type":"object","properties":{"name":{"type":"string"},"icon":{"$ref":"#/$defs/Icon"},"action":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","const":"open_url"},"url":{"type":"string"}},"required":["type","url"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string","const":"continue_conversation"},"context":{"type":"string"}},"required":["type"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string"},"params":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["type"],"additionalProperties":false}]},"variant":{"type":"string","enum":["primary","secondary","tertiary"]},"size":{"type":"string","enum":["extra-small","small","medium","large"]},"shape":{"type":"string","enum":["square","circle"]}},"required":["name","icon"],"additionalProperties":false,"description":"Icon-only button. name is the accessible label and the action label; icon is an Icon; action fires on click."},"Steps":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/$defs/StepsItem"}}},"required":["items"],"additionalProperties":false,"description":"Step-by-step guide"},"StepsItem":{"type":"object","properties":{"title":{"type":"string"},"details":{"type":"string"}},"required":["title","details"],"additionalProperties":false,"description":"title and details text for one step"},"InlineHeader":{"type":"object","properties":{"heading":{"type":"string"},"description":{"type":"string"}},"required":["heading"],"additionalProperties":false,"description":"Compact section heading with an optional one-line description, for use inside cards."},"EntityList":{"type":"object","properties":{"rows":{"default":[],"type":"array","items":{"type":"object","properties":{"left":{"type":"string"},"right":{"type":"string"},"rightVariant":{"default":"text","type":"string","enum":["text","number"]}},"required":["left","right","rightVariant"],"additionalProperties":false}},"size":{"default":"default","type":"string","enum":["small","default"]},"header":{"type":"object","properties":{"left":{"type":"string"},"right":{"type":"string"},"rightVariant":{"default":"text","type":"string","enum":["text","number"]}},"required":["left","right","rightVariant"],"additionalProperties":false},"footer":{"type":"object","properties":{"left":{"type":"string"},"right":{"type":"string"},"rightVariant":{"default":"text","type":"string","enum":["text","number"]}},"required":["left","right","rightVariant"],"additionalProperties":false}},"required":["rows","size"],"additionalProperties":false,"description":"Two-column key/value rows (left label, right value). size 'default' supports optional header and footer rows; rightVariant 'number' uses tabular numbers."},"EditableTable":{"type":"object","properties":{"name":{"default":"","type":"string"},"columns":{"default":[],"type":"array","items":{"type":"object","properties":{"type":{"type":"string","enum":["text","number","date-single","select","url"]},"key":{"default":"default","type":"string"},"header":{"default":"","type":"string"},"width":{"type":"number"},"options":{"type":"array","items":{"type":"object","properties":{"value":{"type":"string"},"label":{"type":"string"}},"required":["value","label"],"additionalProperties":false}}},"required":["type","key","header"],"additionalProperties":false}},"data":{"default":[],"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"values":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}}},"required":["id","values"],"additionalProperties":false}}},"required":["name","columns","data"],"additionalProperties":false,"description":"Spreadsheet-like table whose cells the user can edit inline (text, number, url, date, select columns); edits are saved back as a form field"},"SnippetCardBlock":{"type":"object","properties":{"items":{"minItems":2,"type":"array","items":{"$ref":"#/$defs/SnippetCardItem"}},"layout":{"default":"grid","type":"string","enum":["grid"]},"responsive":{"default":true,"type":"boolean"},"action":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","const":"open_url"},"url":{"type":"string"}},"required":["type","url"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string","const":"continue_conversation"},"context":{"type":"string"}},"required":["type"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string"},"params":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["type"],"additionalProperties":false}]},"gap":{"anyOf":[{"type":"number"},{"type":"string"}]}},"required":["items","layout","responsive"],"additionalProperties":false,"description":"A responsive grid of compact label/value cards (2 per row) for showing several short facts side by side; optionally clickable with a shared action."},"SnippetCardItem":{"type":"object","properties":{"id":{"type":"string"},"lhs":{"anyOf":[{"$ref":"#/$defs/IconText"},{"$ref":"#/$defs/ImageText"}]},"rhs":{"anyOf":[{"$ref":"#/$defs/Text"},{"$ref":"#/$defs/BoldText"}]}},"required":["lhs"],"additionalProperties":false,"description":"One row-style snippet card: a label on the left (IconText or ImageText) and an optional value on the right (Text or BoldText)."},"IconText":{"type":"object","properties":{"icon":{"$ref":"#/$defs/Icon"},"iconVariant":{"default":"neutral","type":"string","enum":["neutral","info","success","warning","danger","inverted","filled","soft"]},"iconSize":{"default":"m","type":"string","enum":["xs","s","m","l","xl","sm","md","lg"]},"title":{"type":"string"},"subtitle":{"type":"string"},"bold":{"default":false,"type":"boolean"},"layout":{"default":"horizontal","type":"string","enum":["horizontal","vertical"]}},"required":["icon","iconVariant","iconSize","title","bold","layout"],"additionalProperties":false,"description":"An icon badge with a title and optional subtitle, laid out horizontally or vertically. iconVariant sets the badge color."},"ImageText":{"type":"object","properties":{"src":{"type":"string"},"alt":{"type":"string"},"title":{"type":"string"},"subtitle":{"type":"string"},"bold":{"default":false,"type":"boolean"},"layout":{"default":"horizontal","type":"string","enum":["horizontal","vertical"]},"imageSize":{"type":"number"}},"required":["src","title","bold","layout"],"additionalProperties":false,"description":"A small square image (thumbnail/avatar) with a title and optional subtitle. src must be a real image URL."},"Text":{"type":"object","properties":{"variant":{"default":"text","type":"string","enum":["text","number"]},"value":{"type":"string"},"subtext":{"type":"string"},"subtextVariant":{"default":"text","type":"string","enum":["text","number","metric"]},"size":{"default":"sm","type":"string","enum":["xs","sm","md","lg"]}},"required":["variant","value","subtextVariant","size"],"additionalProperties":false,"description":"Plain text line with optional subtext. variant 'number' uses tabular number styling; subtextVariant 'metric' colors a leading +/- subtext green/red."},"BoldText":{"type":"object","properties":{"variant":{"default":"text","type":"string","enum":["text","number"]},"value":{"type":"string"},"subtext":{"type":"string"},"subtextVariant":{"default":"text","type":"string","enum":["text","number","metric"]},"size":{"default":"sm","type":"string","enum":["xs","sm","md","lg"]}},"required":["variant","value","subtextVariant","size"],"additionalProperties":false,"description":"Emphasized (bold) text line with optional subtext. variant 'number' uses tabular number styling; subtextVariant 'metric' colors a leading +/- subtext green/red."},"OverviewCardBlock":{"type":"object","properties":{"items":{"minItems":2,"type":"array","items":{"$ref":"#/$defs/OverviewCardItem"}},"layout":{"default":"grid","type":"string","enum":["grid","carousel"]},"responsive":{"default":true,"type":"boolean"},"action":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","const":"open_url"},"url":{"type":"string"}},"required":["type","url"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string","const":"continue_conversation"},"context":{"type":"string"}},"required":["type"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string"},"params":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["type"],"additionalProperties":false}]},"gap":{"anyOf":[{"type":"number"},{"type":"string"}]}},"required":["items","layout","responsive"],"additionalProperties":false,"description":"A grid or horizontal carousel of compact overview cards, each with a heading (icon/image/text) on top and an inline metric below; optionally clickable with a shared action."},"OverviewCardItem":{"type":"object","properties":{"id":{"type":"string"},"top":{"anyOf":[{"$ref":"#/$defs/IconText"},{"$ref":"#/$defs/ImageText"},{"$ref":"#/$defs/Text"}]},"bottom":{"$ref":"#/$defs/MetricIndicatorInline"}},"required":["top"],"additionalProperties":false,"description":"One overview card: a heading slot at the top (IconText, ImageText or Text) and an optional MetricIndicatorInline at the bottom."},"MetricIndicatorInline":{"type":"object","properties":{"value":{"type":"string"},"subtext":{"type":"string"},"trend":{"type":"object","properties":{"direction":{"type":"string","enum":["up","down"]},"value":{"type":"number"}},"required":["direction","value"],"additionalProperties":false}},"required":["value"],"additionalProperties":false,"description":"Headline metric value with an optional +/- percentage trend and subtext, all on one line."},"ContextCardBlock":{"type":"object","properties":{"items":{"minItems":2,"type":"array","items":{"$ref":"#/$defs/ContextCardItem"}},"layout":{"default":"grid","type":"string","enum":["grid","carousel"]},"responsive":{"default":true,"type":"boolean"},"action":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","const":"open_url"},"url":{"type":"string"}},"required":["type","url"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string","const":"continue_conversation"},"context":{"type":"string"}},"required":["type"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string"},"params":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["type"],"additionalProperties":false}]},"gap":{"anyOf":[{"type":"number"},{"type":"string"}]}},"required":["items","layout","responsive"],"additionalProperties":false,"description":"A grid or carousel of compact tinted context cards (title or tag plus a short bold body); an optional action makes every card clickable."},"ContextCardItem":{"type":"object","properties":{"id":{"type":"string"},"title":{"anyOf":[{"type":"string"},{"$ref":"#/$defs/Tag"}]},"body":{"type":"string"},"bgColor":{"type":"string","enum":["gray"]},"bgImageSrc":{"type":"string"},"bgImageAlt":{"type":"string"}},"required":["title"],"additionalProperties":false,"description":"A single card inside a ContextCardBlock: a title (plain string or Tag), an optional markdown body, and an optional gray tint or background image."},"Tag":{"type":"object","properties":{"text":{"type":"string"},"icon":{"$ref":"#/$defs/Icon"},"size":{"type":"string","enum":["sm","md","lg"]},"variant":{"type":"string","enum":["neutral","info","success","warning","danger"]}},"required":["text"],"additionalProperties":false,"description":"Styled tag/badge with optional Icon and variant"},"CompositeCardBlock":{"type":"object","properties":{"items":{"minItems":2,"type":"array","items":{"$ref":"#/$defs/CompositeCardItem"}},"layout":{"default":"grid","type":"string","enum":["grid","carousel"]},"responsive":{"default":true,"type":"boolean"},"action":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","const":"open_url"},"url":{"type":"string"}},"required":["type","url"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string","const":"continue_conversation"},"context":{"type":"string"}},"required":["type"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string"},"params":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["type"],"additionalProperties":false}]},"gap":{"anyOf":[{"type":"number"},{"type":"string"}]}},"required":["items","layout","responsive"],"additionalProperties":false,"description":"A two-per-row grid or carousel of rich cards, each with an optional header, stacked body content (text, metrics, charts, lists, tags) and a price/button footer; an optional action makes every card clickable."},"CompositeCardItem":{"type":"object","properties":{"id":{"type":"string"},"header":{"anyOf":[{"$ref":"#/$defs/IconText"},{"$ref":"#/$defs/ImageText"},{"$ref":"#/$defs/ImageTextLarge"},{"$ref":"#/$defs/Text"},{"$ref":"#/$defs/Image"}]},"body":{"default":[],"type":"array","items":{"anyOf":[{"$ref":"#/$defs/Text"},{"$ref":"#/$defs/BoldText"},{"$ref":"#/$defs/MetricIndicatorInline"},{"$ref":"#/$defs/IconText"},{"$ref":"#/$defs/Image"},{"$ref":"#/$defs/AreaChart"},{"$ref":"#/$defs/BarChart"},{"$ref":"#/$defs/LineChart"},{"$ref":"#/$defs/ListBlock"},{"$ref":"#/$defs/TagBlock"},{"$ref":"#/$defs/EntityList"}]}},"footer":{"type":"object","properties":{"price":{"anyOf":[{"$ref":"#/$defs/BoldText"},{"$ref":"#/$defs/MetricIndicatorWithStrikethrough"}]},"button":{"$ref":"#/$defs/Button"}},"additionalProperties":false}},"required":["body"],"additionalProperties":false,"description":"A single card inside a CompositeCardBlock: an optional header (icon/image/text), a stack of body elements (text, metrics, charts, lists, tags), and an optional price/button footer."},"ImageTextLarge":{"type":"object","properties":{"src":{"type":"string"},"alt":{"type":"string"},"title":{"type":"string"},"subtitle":{"type":"string"},"bold":{"default":false,"type":"boolean"}},"required":["src","title","bold"],"additionalProperties":false,"description":"A full-width banner image above a bold title and optional subtitle. src must be a real image URL."},"ListBlock":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/$defs/ListItem"}},"variant":{"type":"string","enum":["number","image"]},"size":{"type":"string","enum":["default","small"]}},"required":["items"],"additionalProperties":false,"description":"A list of items with number or image indicators. Each item can optionally have an action. size small renders a compact list."},"ListItem":{"type":"object","properties":{"title":{"type":"string"},"subtitle":{"type":"string"},"image":{"type":"object","properties":{"src":{"type":"string"},"alt":{"type":"string"}},"required":["src","alt"],"additionalProperties":false},"actionLabel":{"type":"string"},"action":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","const":"open_url"},"url":{"type":"string"}},"required":["type","url"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string","const":"continue_conversation"},"context":{"type":"string"}},"required":["type"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string"},"params":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["type"],"additionalProperties":false}]}},"required":["title"],"additionalProperties":false,"description":"Item in a ListBlock — displays a title with an optional subtitle and image. When action is provided, the item becomes clickable."},"MetricIndicatorWithStrikethrough":{"type":"object","properties":{"value":{"type":"string"},"subtext":{"type":"string"},"previousValue":{"type":"string"},"trend":{"type":"object","properties":{"direction":{"type":"string","enum":["up","down"]},"value":{"type":"number"}},"required":["direction","value"],"additionalProperties":false}},"required":["value"],"additionalProperties":false,"description":"Headline metric value with an optional struck-through previousValue, a +/- percentage trend, and subtext below."},"VisualCardBlock":{"type":"object","properties":{"items":{"minItems":2,"type":"array","items":{"$ref":"#/$defs/VisualCardItem"}},"layout":{"default":"grid","type":"string","enum":["grid","carousel"]},"responsive":{"default":true,"type":"boolean"},"action":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","const":"open_url"},"url":{"type":"string"}},"required":["type","url"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string","const":"continue_conversation"},"context":{"type":"string"}},"required":["type"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string"},"params":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["type"],"additionalProperties":false}]},"gap":{"anyOf":[{"type":"number"},{"type":"string"}]}},"required":["items","layout","responsive"],"additionalProperties":false,"description":"A grid or carousel of photo-first cards: a full-bleed background image with a tag on top and a bold text panel at the bottom; an optional action makes every card clickable."},"VisualCardItem":{"type":"object","properties":{"body":{"$ref":"#/$defs/BoldText"},"id":{"type":"string"},"bgImageSrc":{"type":"string"},"tag":{"$ref":"#/$defs/Tag"},"bgImageAlt":{"type":"string"}},"required":["body"],"additionalProperties":false,"description":"A single photo-first card inside a VisualCardBlock: a BoldText body panel, an optional Tag, and a background image (bgImageSrc must be a real URL; bgImageAlt is its alt text)."},"Tabs":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/$defs/TabItem"}}},"required":["items"],"additionalProperties":false,"description":"Tabbed container"},"TabItem":{"type":"object","properties":{"value":{"type":"string"},"trigger":{"type":"string"},"content":{"type":"array","items":{"anyOf":[{"$ref":"#/$defs/TextContent"},{"$ref":"#/$defs/MarkDownRenderer"},{"$ref":"#/$defs/CardHeader"},{"$ref":"#/$defs/Callout"},{"$ref":"#/$defs/TextCallout"},{"$ref":"#/$defs/CodeBlock"},{"$ref":"#/$defs/Image"},{"$ref":"#/$defs/ImageBlock"},{"$ref":"#/$defs/ImageGallery"},{"$ref":"#/$defs/Separator"},{"$ref":"#/$defs/HorizontalBarChart"},{"$ref":"#/$defs/RadarChart"},{"$ref":"#/$defs/PieChart"},{"$ref":"#/$defs/RadialChart"},{"$ref":"#/$defs/SingleStackedBarChart"},{"$ref":"#/$defs/ScatterChart"},{"$ref":"#/$defs/AreaChart"},{"$ref":"#/$defs/BarChart"},{"$ref":"#/$defs/LineChart"},{"$ref":"#/$defs/Table"},{"$ref":"#/$defs/TagBlock"},{"$ref":"#/$defs/Form"},{"$ref":"#/$defs/Buttons"},{"$ref":"#/$defs/IconButton"},{"$ref":"#/$defs/Steps"},{"$ref":"#/$defs/InlineHeader"},{"$ref":"#/$defs/EntityList"},{"$ref":"#/$defs/EditableTable"},{"$ref":"#/$defs/SnippetCardBlock"},{"$ref":"#/$defs/OverviewCardBlock"},{"$ref":"#/$defs/ContextCardBlock"},{"$ref":"#/$defs/CompositeCardBlock"},{"$ref":"#/$defs/VisualCardBlock"}]}}},"required":["value","trigger","content"],"additionalProperties":false,"description":"value is unique id, trigger is tab label, content is array of components"},"Carousel":{"type":"object","properties":{"children":{"type":"array","items":{"type":"array","items":{"anyOf":[{"$ref":"#/$defs/TextContent"},{"$ref":"#/$defs/MarkDownRenderer"},{"$ref":"#/$defs/CardHeader"},{"$ref":"#/$defs/Callout"},{"$ref":"#/$defs/TextCallout"},{"$ref":"#/$defs/CodeBlock"},{"$ref":"#/$defs/Image"},{"$ref":"#/$defs/ImageBlock"},{"$ref":"#/$defs/ImageGallery"},{"$ref":"#/$defs/Separator"},{"$ref":"#/$defs/HorizontalBarChart"},{"$ref":"#/$defs/RadarChart"},{"$ref":"#/$defs/PieChart"},{"$ref":"#/$defs/RadialChart"},{"$ref":"#/$defs/SingleStackedBarChart"},{"$ref":"#/$defs/ScatterChart"},{"$ref":"#/$defs/AreaChart"},{"$ref":"#/$defs/BarChart"},{"$ref":"#/$defs/LineChart"},{"$ref":"#/$defs/Table"},{"$ref":"#/$defs/TagBlock"},{"$ref":"#/$defs/Form"},{"$ref":"#/$defs/Buttons"},{"$ref":"#/$defs/IconButton"},{"$ref":"#/$defs/Steps"},{"$ref":"#/$defs/InlineHeader"},{"$ref":"#/$defs/EntityList"},{"$ref":"#/$defs/EditableTable"},{"$ref":"#/$defs/SnippetCardBlock"},{"$ref":"#/$defs/OverviewCardBlock"},{"$ref":"#/$defs/ContextCardBlock"},{"$ref":"#/$defs/CompositeCardBlock"},{"$ref":"#/$defs/VisualCardBlock"}]}}},"variant":{"type":"string","enum":["card","sunk"]}},"required":["children"],"additionalProperties":false,"description":"Horizontal scrollable carousel"},"Stack":{"type":"object","properties":{"children":{"type":"array","items":{}},"direction":{"type":"string","enum":["row","column"]},"gap":{"type":"string","enum":["none","xs","s","m","l","xl","2xl"]},"align":{"type":"string","enum":["start","center","end","stretch","baseline"]},"justify":{"type":"string","enum":["start","center","end","between","around","evenly"]},"wrap":{"type":"boolean"}},"required":["children"],"additionalProperties":false,"description":"Flex container. direction: \"row\"|\"column\" (default \"column\"). gap: \"none\"|\"xs\"|\"s\"|\"m\"|\"l\"|\"xl\"|\"2xl\" (default \"m\"). align: \"start\"|\"center\"|\"end\"|\"stretch\"|\"baseline\". justify: \"start\"|\"center\"|\"end\"|\"between\"|\"around\"|\"evenly\"."},"Slice":{"type":"object","properties":{"category":{"type":"string"},"value":{"type":"number"}},"required":["category","value"],"additionalProperties":false,"description":"One slice with label and numeric value"},"Label":{"type":"object","properties":{"text":{"type":"string"}},"required":["text"],"additionalProperties":false,"description":"Text label"},"SwitchGroup":{"type":"object","properties":{"name":{"type":"string"},"items":{"type":"array","items":{"$ref":"#/$defs/SwitchItem"}},"variant":{"type":"string","enum":["clear","card","sunk"]},"value":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"boolean"}}},"required":["name","items"],"additionalProperties":false,"description":"Group of switch toggles"},"SwitchItem":{"type":"object","properties":{"label":{"type":"string"},"description":{"type":"string"},"name":{"type":"string"},"defaultChecked":{"type":"boolean"}},"required":["name"],"additionalProperties":false,"description":"Individual switch toggle"},"Accordion":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/$defs/AccordionItem"}}},"required":["items"],"additionalProperties":false,"description":"Collapsible sections"},"AccordionItem":{"type":"object","properties":{"value":{"type":"string"},"trigger":{"type":"string"},"content":{"type":"array","items":{"anyOf":[{"$ref":"#/$defs/TextContent"},{"$ref":"#/$defs/MarkDownRenderer"},{"$ref":"#/$defs/CardHeader"},{"$ref":"#/$defs/Callout"},{"$ref":"#/$defs/TextCallout"},{"$ref":"#/$defs/CodeBlock"},{"$ref":"#/$defs/Image"},{"$ref":"#/$defs/ImageBlock"},{"$ref":"#/$defs/ImageGallery"},{"$ref":"#/$defs/Separator"},{"$ref":"#/$defs/HorizontalBarChart"},{"$ref":"#/$defs/RadarChart"},{"$ref":"#/$defs/PieChart"},{"$ref":"#/$defs/RadialChart"},{"$ref":"#/$defs/SingleStackedBarChart"},{"$ref":"#/$defs/ScatterChart"},{"$ref":"#/$defs/AreaChart"},{"$ref":"#/$defs/BarChart"},{"$ref":"#/$defs/LineChart"},{"$ref":"#/$defs/Table"},{"$ref":"#/$defs/TagBlock"},{"$ref":"#/$defs/Form"},{"$ref":"#/$defs/Buttons"},{"$ref":"#/$defs/IconButton"},{"$ref":"#/$defs/Steps"},{"$ref":"#/$defs/InlineHeader"},{"$ref":"#/$defs/EntityList"},{"$ref":"#/$defs/EditableTable"},{"$ref":"#/$defs/SnippetCardBlock"},{"$ref":"#/$defs/OverviewCardBlock"},{"$ref":"#/$defs/ContextCardBlock"},{"$ref":"#/$defs/CompositeCardBlock"},{"$ref":"#/$defs/VisualCardBlock"}]}}},"required":["value","trigger","content"],"additionalProperties":false,"description":"value is unique id, trigger is section title"},"Modal":{"type":"object","properties":{"title":{"type":"string"},"open":{"type":"boolean"},"children":{"type":"array","items":{"anyOf":[{"$ref":"#/$defs/TextContent"},{"$ref":"#/$defs/MarkDownRenderer"},{"$ref":"#/$defs/CardHeader"},{"$ref":"#/$defs/Callout"},{"$ref":"#/$defs/TextCallout"},{"$ref":"#/$defs/CodeBlock"},{"$ref":"#/$defs/Image"},{"$ref":"#/$defs/ImageBlock"},{"$ref":"#/$defs/ImageGallery"},{"$ref":"#/$defs/Separator"},{"$ref":"#/$defs/HorizontalBarChart"},{"$ref":"#/$defs/RadarChart"},{"$ref":"#/$defs/PieChart"},{"$ref":"#/$defs/RadialChart"},{"$ref":"#/$defs/SingleStackedBarChart"},{"$ref":"#/$defs/ScatterChart"},{"$ref":"#/$defs/AreaChart"},{"$ref":"#/$defs/BarChart"},{"$ref":"#/$defs/LineChart"},{"$ref":"#/$defs/Table"},{"$ref":"#/$defs/TagBlock"},{"$ref":"#/$defs/Form"},{"$ref":"#/$defs/Buttons"},{"$ref":"#/$defs/IconButton"},{"$ref":"#/$defs/Steps"},{"$ref":"#/$defs/InlineHeader"},{"$ref":"#/$defs/EntityList"},{"$ref":"#/$defs/EditableTable"},{"$ref":"#/$defs/SnippetCardBlock"},{"$ref":"#/$defs/OverviewCardBlock"},{"$ref":"#/$defs/ContextCardBlock"},{"$ref":"#/$defs/CompositeCardBlock"},{"$ref":"#/$defs/VisualCardBlock"}]}},"size":{"type":"string","enum":["sm","md","lg"]}},"required":["title","children"],"additionalProperties":false,"description":"Modal dialog. open is a reactive $boolean binding — set to true to open, X/Escape/backdrop auto-closes. Put Form with buttons inside children."}}} +} diff --git a/packages/swift-lang/Tests/OpenUILangTests/Fixtures/parser.json b/packages/swift-lang/Tests/OpenUILangTests/Fixtures/parser.json new file mode 100644 index 0000000000..f12909222c --- /dev/null +++ b/packages/swift-lang/Tests/OpenUILangTests/Fixtures/parser.json @@ -0,0 +1,67 @@ +[ +{"name":"chat-example-1","schema":"chat","input":"Example 1 — Informational response (the shape for \"tell me about X\" / explainer asks):\n\nroot = Card([header, intro, highlights, statsHeader, statsCards, sectionsBlock, actionBtns], [src1, src2])\nheader = CardHeader(\"Paris\", \"The City of Light · France's Eternal Capital\")\nintro = TextContent(\"Paris is one of the world's most visited cities — a timeless blend of iconic landmarks, world-class cuisine, art, and romance [1]. In 2026 the city is buzzing with new energy: the **Grand Palais** has reopened after a €466M renovation, and you can now **swim in the Seine** for the first time in over a century [2].\")\nhighlights = ContextCardBlock([h1, h2, h3], \"grid\", true, { type: \"continue_conversation\", context: \"Tell me more about this aspect of Paris\" })\nh1 = ContextCardItem(\"art-museums\", \"Art & Museums\", \"143 museums including the Louvre & newly reopened Grand Palais.\", \"gray\", \"https://cdn.britannica.com/03/121003-050-2544BD4E/Interior-Louvre-Museum-Paris.jpg\")\nh2 = ContextCardItem(\"gastronomy\", \"Gastronomy\", \"9,000+ restaurants, 130+ Michelin stars, and the world's best baguettes.\", \"gray\")\nh3 = ContextCardItem(\"fashion-capital\", \"Fashion Capital\", \"Home to Chanel, Dior, Louis Vuitton & Hermès — the global luxury hub.\", \"gray\", \"https://thumbs.dreamstime.com/z/dior-storefront-facade-chanel-store-lvmh-s-french-designer-modehouse-christian-adjacent-to-luxury-fashion-beauty-brand-327161553.jpg\")\nstatsHeader = InlineHeader(\"Paris by the Numbers\", \"Key visitor statistics\")\nstatsCards = OverviewCardBlock([ov1, ov2, ov3], \"grid\", true)\nov1 = OverviewCardItem(\"annual-visitors\", ov1top, ov1metric)\nov1top = IconText(ov1icon, \"neutral\", \"m\", \"Annual Visitors\", \"2024 arrivals\", false, \"vertical\")\nov1icon = Icon(\"users\", \"people\")\nov1metric = MetricIndicatorInline(\"48.7M\", \"tourists\", { direction: \"up\", value: 2.5 })\nov2 = OverviewCardItem(\"museums-monuments\", ov2top, ov2metric)\nov2top = IconText(ov2icon, \"neutral\", \"m\", \"Museums & Monuments\", \"Cultural sites\", false, \"vertical\")\nov2icon = Icon(\"landmark\", \"buildings\")\nov2metric = MetricIndicatorInline(\"2,370+\", \"across the city\")\nov3 = OverviewCardItem(\"tourism-revenue\", ov3top, ov3metric)\nov3top = IconText(ov3icon, \"neutral\", \"m\", \"Tourism Revenue\", \"2024 estimate\", false, \"vertical\")\nov3icon = Icon(\"circle-dollar-sign\", \"finance\")\nov3metric = MetricIndicatorInline(\"€23.4B\", \"generated\")\nsectionsBlock = SectionBlock([secLandmarks, secFood, secWhen], false)\nsecLandmarks = SectionItem(\"landmarks\", \"Iconic Landmarks\", [landmarksCarousel])\nlandmarksCarousel = VisualCardBlock([lm1, lm2, lm3, lm4], \"carousel\", true, { type: \"continue_conversation\", context: \"Tell me more about this Paris landmark\" })\nlm1 = VisualCardItem(lm1body, \"eiffel-tower\", \"https://c8.alamy.com/comp/RYEB13/aerial-view-of-the-eiffel-tower-with-the-park-champ-de-mars-and-the-river-seine-paris-france-RYEB13.jpg\", lm1tag, \"Aerial view of the Eiffel Tower\")\nlm1body = BoldText(\"text\", \"Eiffel Tower\", \"Open daily 9am–midnight\")\nlm1tag = Tag(\"Must-See\", lm1tagIcon, \"sm\", \"info\")\nlm1tagIcon = Icon(\"star\", \"shapes\")\nlm2 = VisualCardItem(lm2body, \"the-louvre\", \"https://images.fineartamerica.com/images/artworkimages/mediumlarge/3/1-exterior-of-musee-du-louvre-museum-paris-france-bernard-jaubert.jpg\", lm2tag, \"Exterior of the Louvre museum\")\nlm2body = BoldText(\"text\", \"The Louvre\", \"World's largest art museum\")\nlm2tag = Tag(\"8.9M visits/yr\", lm2tagIcon, \"sm\", \"success\")\nlm2tagIcon = Icon(\"trending-up\", \"charts\")\nlm3 = VisualCardItem(lm3body, \"notre-dame\", \"https://cdn.britannica.com/29/255529-050-63A22A3C/notre-dame-de-paris-cathedral-paris-france.jpg\", lm3tag, \"Notre-Dame cathedral facade\")\nlm3body = BoldText(\"text\", \"Notre-Dame\", \"Fully restored cathedral\")\nlm3tag = Tag(\"Reopened 2024\", lm3tagIcon, \"sm\", \"warning\")\nlm3tagIcon = Icon(\"hammer\", \"tools\")\nlm4 = VisualCardItem(lm4body, \"versailles\", \"https://cdn.britannica.com/76/130076-050-31128A3D/Latona-Fountain-Balthazard-Marsy-Gaspard-Andre-Le.jpg\", lm4tag, \"Latona Fountain at Versailles\")\nlm4body = BoldText(\"text\", \"Palace of Versailles\", \"Royal gardens & grand halls\")\nlm4tag = Tag(\"Day Trip\", lm4tagIcon, \"sm\", \"neutral\")\nlm4tagIcon = Icon(\"train-front\", \"travel\")\nsecFood = SectionItem(\"food\", \"Food & Drink\", [foodCards])\nfoodCards = ContextCardBlock([f1, f2, f3], \"grid\", true, { type: \"continue_conversation\", context: \"Tell me more about eating and drinking in Paris\" })\nf1 = ContextCardItem(\"bistros\", \"Bistros\", \"Classic French fare — try Les Arlots for confit beef cheeks.\", \"gray\", \"https://everydayparisian.com/wp-content/uploads/2023/01/IMG_0221-768x1024.webp\")\nf2 = ContextCardItem(\"fine-dining\", \"Fine Dining\", \"Septime leads a new wave of creative Michelin-starred cuisine.\", \"gray\", \"https://production-data.worldofmouth.app/images/72b2e6db-97ea-49c4-aa5d-3b7c8121c812.jpg\")\nf3 = ContextCardItem(\"bakeries\", \"Bakeries\", \"Shinya Pain for sourdough. Grand Prix-winning baguettes citywide.\", \"gray\", \"https://www.davidlebovitz.com/wp-content/uploads/2018/09/Le-petit-grain-paris-bakery-boulangerie-patisserie-pastry-shop-8-640x895.jpg\")\nsecWhen = SectionItem(\"when\", \"Best Time to Visit\", [whenTabs])\nwhenTabs = Tabs([tabSpring, tabSummer])\ntabSpring = TabItem(\"spring\", \"Spring\", [springList])\nspringList = ListBlock([sp1, sp2, sp3], \"number\")\nsp1 = ListItem(\"Cherry blossoms at Parc de Sceaux\", \"March – May\", null, null, { type: \"continue_conversation\", context: \"Tell me about cherry blossom season in Paris\" })\nsp2 = ListItem(\"Picnics along the Seine & in city parks\", \"Warm, lively atmosphere\", null, null, { type: \"continue_conversation\", context: \"Where are the best picnic spots in Paris?\" })\nsp3 = ListItem(\"Fewer crowds than summer\", \"Ideal for sightseeing\", null, null, { type: \"continue_conversation\", context: \"How busy is Paris in spring?\" })\ntabSummer = TabItem(\"summer\", \"Summer\", [summerList])\nsummerList = ListBlock([su1, su2, su3], \"number\")\nsu1 = ListItem(\"Swim in the Seine — open July & August 2026\", \"First time in 102 years\", null, null, { type: \"continue_conversation\", context: \"Tell me about swimming in the Seine\" })\nsu2 = ListItem(\"Open-air cinema at Parc de la Villette\", \"Free films under the stars\", null, null, { type: \"continue_conversation\", context: \"Tell me about the open-air cinema at Parc de la Villette\" })\nsu3 = ListItem(\"Bastille Day celebrations on July 14\", \"Fireworks at the Eiffel Tower\", null, null, { type: \"continue_conversation\", context: \"What happens in Paris on Bastille Day?\" })\nactionBtns = Buttons([btn1, btn2, btn3], \"row\")\nbtn1 = Button(\"Plan a Trip to Paris\", { type: \"continue_conversation\", context: \"Help me plan a trip to Paris\" }, \"primary\")\nbtn2 = Button(\"Best Hotels in Paris\", { type: \"continue_conversation\", context: \"Show me the best hotels in Paris\" }, \"secondary\")\nbtn3 = Button(\"Build a Paris Itinerary\", { type: \"continue_conversation\", context: \"Build a 5-day Paris itinerary for me\" }, \"secondary\")\nsrc1 = { title: \"Paris Travel Guide 2025\", sourceName: \"Travel and Tour World\", url: \"https://www.travelandtourworld.com/news/article/from-iconic-landmarks-to-hidden-gems-the-ultimate-paris-travel-guide-for-2025-you-need-to-read-now/\" }\nsrc2 = { title: \"Best Things to Do in Paris 2026\", sourceName: \"Time Out Paris\", url: \"http://www.timeout.fr/paris/en/for-tourists\" }","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Paris","subtitle":"The City of Light · France's Eternal Capital"},"partial":false,"hasDynamicProps":false,"statementId":"header"},{"type":"element","typeName":"TextContent","props":{"text":"Paris is one of the world's most visited cities — a timeless blend of iconic landmarks, world-class cuisine, art, and romance [1]. In 2026 the city is buzzing with new energy: the **Grand Palais** has reopened after a €466M renovation, and you can now **swim in the Seine** for the first time in over a century [2]."},"partial":false,"hasDynamicProps":false,"statementId":"intro"},{"type":"element","typeName":"ContextCardBlock","props":{"items":[{"type":"element","typeName":"ContextCardItem","props":{"id":"art-museums","title":"Art & Museums","body":"143 museums including the Louvre & newly reopened Grand Palais.","bgColor":"gray","bgImageSrc":"https://cdn.britannica.com/03/121003-050-2544BD4E/Interior-Louvre-Museum-Paris.jpg"},"partial":false,"hasDynamicProps":false,"statementId":"h1"},{"type":"element","typeName":"ContextCardItem","props":{"id":"gastronomy","title":"Gastronomy","body":"9,000+ restaurants, 130+ Michelin stars, and the world's best baguettes.","bgColor":"gray"},"partial":false,"hasDynamicProps":false,"statementId":"h2"},{"type":"element","typeName":"ContextCardItem","props":{"id":"fashion-capital","title":"Fashion Capital","body":"Home to Chanel, Dior, Louis Vuitton & Hermès — the global luxury hub.","bgColor":"gray","bgImageSrc":"https://thumbs.dreamstime.com/z/dior-storefront-facade-chanel-store-lvmh-s-french-designer-modehouse-christian-adjacent-to-luxury-fashion-beauty-brand-327161553.jpg"},"partial":false,"hasDynamicProps":false,"statementId":"h3"}],"layout":"grid","responsive":true,"action":{"type":"continue_conversation","context":"Tell me more about this aspect of Paris"}},"partial":false,"hasDynamicProps":false,"statementId":"highlights"},{"type":"element","typeName":"InlineHeader","props":{"heading":"Paris by the Numbers","description":"Key visitor statistics"},"partial":false,"hasDynamicProps":false,"statementId":"statsHeader"},{"type":"element","typeName":"OverviewCardBlock","props":{"items":[{"type":"element","typeName":"OverviewCardItem","props":{"id":"annual-visitors","top":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"users","category":"people"},"partial":false,"hasDynamicProps":false,"statementId":"ov1icon"},"iconVariant":"neutral","iconSize":"m","title":"Annual Visitors","subtitle":"2024 arrivals","bold":false,"layout":"vertical"},"partial":false,"hasDynamicProps":false,"statementId":"ov1top"},"bottom":{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"48.7M","subtext":"tourists","trend":{"direction":"up","value":2.5}},"partial":false,"hasDynamicProps":false,"statementId":"ov1metric"}},"partial":false,"hasDynamicProps":false,"statementId":"ov1"},{"type":"element","typeName":"OverviewCardItem","props":{"id":"museums-monuments","top":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"landmark","category":"buildings"},"partial":false,"hasDynamicProps":false,"statementId":"ov2icon"},"iconVariant":"neutral","iconSize":"m","title":"Museums & Monuments","subtitle":"Cultural sites","bold":false,"layout":"vertical"},"partial":false,"hasDynamicProps":false,"statementId":"ov2top"},"bottom":{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"2,370+","subtext":"across the city"},"partial":false,"hasDynamicProps":false,"statementId":"ov2metric"}},"partial":false,"hasDynamicProps":false,"statementId":"ov2"},{"type":"element","typeName":"OverviewCardItem","props":{"id":"tourism-revenue","top":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"circle-dollar-sign","category":"finance"},"partial":false,"hasDynamicProps":false,"statementId":"ov3icon"},"iconVariant":"neutral","iconSize":"m","title":"Tourism Revenue","subtitle":"2024 estimate","bold":false,"layout":"vertical"},"partial":false,"hasDynamicProps":false,"statementId":"ov3top"},"bottom":{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"€23.4B","subtext":"generated"},"partial":false,"hasDynamicProps":false,"statementId":"ov3metric"}},"partial":false,"hasDynamicProps":false,"statementId":"ov3"}],"layout":"grid","responsive":true},"partial":false,"hasDynamicProps":false,"statementId":"statsCards"},{"type":"element","typeName":"SectionBlock","props":{"sections":[{"type":"element","typeName":"SectionItem","props":{"value":"landmarks","trigger":"Iconic Landmarks","content":[{"type":"element","typeName":"VisualCardBlock","props":{"items":[{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Eiffel Tower","subtext":"Open daily 9am–midnight","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"lm1body"},"id":"eiffel-tower","bgImageSrc":"https://c8.alamy.com/comp/RYEB13/aerial-view-of-the-eiffel-tower-with-the-park-champ-de-mars-and-the-river-seine-paris-france-RYEB13.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Must-See","icon":{"type":"element","typeName":"Icon","props":{"name":"star","category":"shapes"},"partial":false,"hasDynamicProps":false,"statementId":"lm1tagIcon"},"size":"sm","variant":"info"},"partial":false,"hasDynamicProps":false,"statementId":"lm1tag"},"bgImageAlt":"Aerial view of the Eiffel Tower"},"partial":false,"hasDynamicProps":false,"statementId":"lm1"},{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"The Louvre","subtext":"World's largest art museum","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"lm2body"},"id":"the-louvre","bgImageSrc":"https://images.fineartamerica.com/images/artworkimages/mediumlarge/3/1-exterior-of-musee-du-louvre-museum-paris-france-bernard-jaubert.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"8.9M visits/yr","icon":{"type":"element","typeName":"Icon","props":{"name":"trending-up","category":"charts"},"partial":false,"hasDynamicProps":false,"statementId":"lm2tagIcon"},"size":"sm","variant":"success"},"partial":false,"hasDynamicProps":false,"statementId":"lm2tag"},"bgImageAlt":"Exterior of the Louvre museum"},"partial":false,"hasDynamicProps":false,"statementId":"lm2"},{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Notre-Dame","subtext":"Fully restored cathedral","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"lm3body"},"id":"notre-dame","bgImageSrc":"https://cdn.britannica.com/29/255529-050-63A22A3C/notre-dame-de-paris-cathedral-paris-france.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Reopened 2024","icon":{"type":"element","typeName":"Icon","props":{"name":"hammer","category":"tools"},"partial":false,"hasDynamicProps":false,"statementId":"lm3tagIcon"},"size":"sm","variant":"warning"},"partial":false,"hasDynamicProps":false,"statementId":"lm3tag"},"bgImageAlt":"Notre-Dame cathedral facade"},"partial":false,"hasDynamicProps":false,"statementId":"lm3"},{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Palace of Versailles","subtext":"Royal gardens & grand halls","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"lm4body"},"id":"versailles","bgImageSrc":"https://cdn.britannica.com/76/130076-050-31128A3D/Latona-Fountain-Balthazard-Marsy-Gaspard-Andre-Le.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Day Trip","icon":{"type":"element","typeName":"Icon","props":{"name":"train-front","category":"travel"},"partial":false,"hasDynamicProps":false,"statementId":"lm4tagIcon"},"size":"sm","variant":"neutral"},"partial":false,"hasDynamicProps":false,"statementId":"lm4tag"},"bgImageAlt":"Latona Fountain at Versailles"},"partial":false,"hasDynamicProps":false,"statementId":"lm4"}],"layout":"carousel","responsive":true,"action":{"type":"continue_conversation","context":"Tell me more about this Paris landmark"}},"partial":false,"hasDynamicProps":false,"statementId":"landmarksCarousel"}]},"partial":false,"hasDynamicProps":false,"statementId":"secLandmarks"},{"type":"element","typeName":"SectionItem","props":{"value":"food","trigger":"Food & Drink","content":[{"type":"element","typeName":"ContextCardBlock","props":{"items":[{"type":"element","typeName":"ContextCardItem","props":{"id":"bistros","title":"Bistros","body":"Classic French fare — try Les Arlots for confit beef cheeks.","bgColor":"gray","bgImageSrc":"https://everydayparisian.com/wp-content/uploads/2023/01/IMG_0221-768x1024.webp"},"partial":false,"hasDynamicProps":false,"statementId":"f1"},{"type":"element","typeName":"ContextCardItem","props":{"id":"fine-dining","title":"Fine Dining","body":"Septime leads a new wave of creative Michelin-starred cuisine.","bgColor":"gray","bgImageSrc":"https://production-data.worldofmouth.app/images/72b2e6db-97ea-49c4-aa5d-3b7c8121c812.jpg"},"partial":false,"hasDynamicProps":false,"statementId":"f2"},{"type":"element","typeName":"ContextCardItem","props":{"id":"bakeries","title":"Bakeries","body":"Shinya Pain for sourdough. Grand Prix-winning baguettes citywide.","bgColor":"gray","bgImageSrc":"https://www.davidlebovitz.com/wp-content/uploads/2018/09/Le-petit-grain-paris-bakery-boulangerie-patisserie-pastry-shop-8-640x895.jpg"},"partial":false,"hasDynamicProps":false,"statementId":"f3"}],"layout":"grid","responsive":true,"action":{"type":"continue_conversation","context":"Tell me more about eating and drinking in Paris"}},"partial":false,"hasDynamicProps":false,"statementId":"foodCards"}]},"partial":false,"hasDynamicProps":false,"statementId":"secFood"},{"type":"element","typeName":"SectionItem","props":{"value":"when","trigger":"Best Time to Visit","content":[{"type":"element","typeName":"Tabs","props":{"items":[{"type":"element","typeName":"TabItem","props":{"value":"spring","trigger":"Spring","content":[{"type":"element","typeName":"ListBlock","props":{"items":[{"type":"element","typeName":"ListItem","props":{"title":"Cherry blossoms at Parc de Sceaux","subtitle":"March – May","image":null,"actionLabel":null,"action":{"type":"continue_conversation","context":"Tell me about cherry blossom season in Paris"}},"partial":false,"hasDynamicProps":false,"statementId":"sp1"},{"type":"element","typeName":"ListItem","props":{"title":"Picnics along the Seine & in city parks","subtitle":"Warm, lively atmosphere","image":null,"actionLabel":null,"action":{"type":"continue_conversation","context":"Where are the best picnic spots in Paris?"}},"partial":false,"hasDynamicProps":false,"statementId":"sp2"},{"type":"element","typeName":"ListItem","props":{"title":"Fewer crowds than summer","subtitle":"Ideal for sightseeing","image":null,"actionLabel":null,"action":{"type":"continue_conversation","context":"How busy is Paris in spring?"}},"partial":false,"hasDynamicProps":false,"statementId":"sp3"}],"variant":"number"},"partial":false,"hasDynamicProps":false,"statementId":"springList"}]},"partial":false,"hasDynamicProps":false,"statementId":"tabSpring"},{"type":"element","typeName":"TabItem","props":{"value":"summer","trigger":"Summer","content":[{"type":"element","typeName":"ListBlock","props":{"items":[{"type":"element","typeName":"ListItem","props":{"title":"Swim in the Seine — open July & August 2026","subtitle":"First time in 102 years","image":null,"actionLabel":null,"action":{"type":"continue_conversation","context":"Tell me about swimming in the Seine"}},"partial":false,"hasDynamicProps":false,"statementId":"su1"},{"type":"element","typeName":"ListItem","props":{"title":"Open-air cinema at Parc de la Villette","subtitle":"Free films under the stars","image":null,"actionLabel":null,"action":{"type":"continue_conversation","context":"Tell me about the open-air cinema at Parc de la Villette"}},"partial":false,"hasDynamicProps":false,"statementId":"su2"},{"type":"element","typeName":"ListItem","props":{"title":"Bastille Day celebrations on July 14","subtitle":"Fireworks at the Eiffel Tower","image":null,"actionLabel":null,"action":{"type":"continue_conversation","context":"What happens in Paris on Bastille Day?"}},"partial":false,"hasDynamicProps":false,"statementId":"su3"}],"variant":"number"},"partial":false,"hasDynamicProps":false,"statementId":"summerList"}]},"partial":false,"hasDynamicProps":false,"statementId":"tabSummer"}]},"partial":false,"hasDynamicProps":false,"statementId":"whenTabs"}]},"partial":false,"hasDynamicProps":false,"statementId":"secWhen"}],"isFoldable":false},"partial":false,"hasDynamicProps":false,"statementId":"sectionsBlock"},{"type":"element","typeName":"Buttons","props":{"buttons":[{"type":"element","typeName":"Button","props":{"label":"Plan a Trip to Paris","action":{"type":"continue_conversation","context":"Help me plan a trip to Paris"},"variant":"primary"},"partial":false,"hasDynamicProps":false,"statementId":"btn1"},{"type":"element","typeName":"Button","props":{"label":"Best Hotels in Paris","action":{"type":"continue_conversation","context":"Show me the best hotels in Paris"},"variant":"secondary"},"partial":false,"hasDynamicProps":false,"statementId":"btn2"},{"type":"element","typeName":"Button","props":{"label":"Build a Paris Itinerary","action":{"type":"continue_conversation","context":"Build a 5-day Paris itinerary for me"},"variant":"secondary"},"partial":false,"hasDynamicProps":false,"statementId":"btn3"}],"direction":"row"},"partial":false,"hasDynamicProps":false,"statementId":"actionBtns"}],"sources":[{"title":"Paris Travel Guide 2025","sourceName":"Travel and Tour World","url":"https://www.travelandtourworld.com/news/article/from-iconic-landmarks-to-hidden-gems-the-ultimate-paris-travel-guide-for-2025-you-need-to-read-now/"},{"title":"Best Things to Do in Paris 2026","sourceName":"Time Out Paris","url":"http://www.timeout.fr/paris/en/for-tourists"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":63,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"chat-example-2","schema":"chat","input":"Example 2 — Data and analytics response (quantitative asks lead with KPIs and charts):\n\nroot = Card([pageHeader, kpiBlock, sections, deepDiveHeader, compositeCards, actionButtons])\npageHeader = CardHeader(\"Analytics Dashboard\", \"April 2026 · Business Performance Overview\")\nkpiBlock = SnippetCardBlock([kpi1, kpi2, kpi3, kpi4], \"grid\", true)\nkpi1 = SnippetCardItem(\"total-revenue\", kpi1lhs, kpi1rhs)\nkpi1lhs = IconText(kpi1icon, \"neutral\", \"m\", \"Total Revenue\", \"Month to date\", false, \"horizontal\")\nkpi1icon = Icon(\"circle-dollar-sign\", \"finance\")\nkpi1rhs = BoldText(\"number\", \"$284,500\", \"+12.4%\", \"metric\")\nkpi2 = SnippetCardItem(\"active-users\", kpi2lhs, kpi2rhs)\nkpi2lhs = IconText(kpi2icon, \"neutral\", \"m\", \"Active Users\", \"Monthly active users\", false, \"horizontal\")\nkpi2icon = Icon(\"users\", \"people\")\nkpi2rhs = BoldText(\"number\", \"47,320\", \"+6.8%\", \"metric\")\nkpi3 = SnippetCardItem(\"conversion-rate\", kpi3lhs, kpi3rhs)\nkpi3lhs = IconText(kpi3icon, \"neutral\", \"m\", \"Conversion Rate\", \"Avg. this month\", false, \"horizontal\")\nkpi3icon = Icon(\"trending-up\", \"charts\")\nkpi3rhs = BoldText(\"number\", \"5.7%\", \"+0.9%\", \"metric\")\nkpi4 = SnippetCardItem(\"churn-rate\", kpi4lhs, kpi4rhs)\nkpi4lhs = IconText(kpi4icon, \"neutral\", \"m\", \"Churn Rate\", \"Monthly customer churn\", false, \"horizontal\")\nkpi4icon = Icon(\"user-minus\", \"people\")\nkpi4rhs = BoldText(\"number\", \"2.1%\", \"-0.4%\", \"metric\")\nsections = SectionBlock([secUsers, secChannels], false)\nsecUsers = SectionItem(\"users\", \"User Activity\", [userTabs])\nuserTabs = Tabs([tabGrowth, tabRetention])\ntabGrowth = TabItem(\"growth\", \"User Growth\", [growthChart])\ngrowthChart = LineChart(months, [dauSeries, mauSeries], \"natural\", \"Month\", \"Users (k)\")\nmonths = [\"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\", \"Jan\", \"Feb\", \"Mar\", \"Apr\"]\ndauSeries = Series(\"DAU\", [5.1, 5.4, 5.8, 6.2, 6.7, 7.1, 7.6, 8.0, 8.4, 8.9, 9.3, 9.8])\nmauSeries = Series(\"MAU\", [28.4, 30.1, 31.8, 33.5, 35.2, 37.1, 38.8, 40.4, 42.1, 43.9, 45.6, 47.3])\ntabRetention = TabItem(\"retention\", \"Cohort Retention\", [retentionChart])\nretentionChart = LineChart(cohortMonths, [cohortJan, cohortFeb, cohortMar], \"natural\", \"Month Since Signup\", \"Retention (%)\")\ncohortMonths = [\"Month 0\", \"Month 1\", \"Month 2\", \"Month 3\", \"Month 4\", \"Month 5\", \"Month 6\"]\ncohortJan = Series(\"Jan Cohort\", [100, 80, 70, 62, 56, 51, 47])\ncohortFeb = Series(\"Feb Cohort\", [100, 83, 73, 65, 59, 54, 50])\ncohortMar = Series(\"Mar Cohort\", [100, 86, 76, 68, 62, 0, 0])\nsecChannels = SectionItem(\"channels\", \"Acquisition Channels\", [channelTabs])\nchannelTabs = Tabs([tabChannelBar, tabChannelTable])\ntabChannelBar = TabItem(\"channel-bar\", \"Revenue by Channel\", [channelBarChart])\nchannelBarChart = HorizontalBarChart(channels, [channelSeries], \"grouped\", \"Revenue ($k)\", \"Channel\")\nchannels = [\"Organic Search\", \"Paid Ads\", \"Referral Program\", \"Partner Network\", \"Direct / Brand\", \"Email Campaigns\"]\nchannelSeries = Series(\"Revenue ($k)\", [82.4, 68.1, 54.6, 38.2, 26.8, 14.4])\ntabChannelTable = TabItem(\"channel-table\", \"Full Breakdown\", [channelTable])\nchannelTable = Table([colChannel, colLeads, colConv, colCac, colRevenue, colStatus])\ncolChannel = Col(\"Channel\", channels, \"string\")\ncolLeads = Col(\"Leads\", [4210, 3180, 2050, 1120, 1290, 820], \"number\")\ncolConv = Col(\"Conv. Rate\", [\"6.4%\", \"5.1%\", \"9.4%\", \"11.8%\", \"5.8%\", \"8.1%\"], \"string\")\ncolCac = Col(\"CAC ($)\", [188, 395, 132, 109, 218, 154], \"number\")\ncolRevenue = Col(\"Revenue ($k)\", [82.4, 68.1, 54.6, 38.2, 26.8, 14.4], \"number\")\ncolStatus = Col(\"Status\", [\"Scaling\", \"Optimizing\", \"Scaling\", \"Growing\", \"Stable\", \"Stable\"], \"string\")\ndeepDiveHeader = InlineHeader(\"Support Health & Marketing ROI\", \"Click a card to explore deeper insights\")\ncompositeCards = CompositeCardBlock([cardSupport, cardMarketing], \"grid\", true, { type: \"continue_conversation\", context: \"Explore this deep-dive area in more detail\" })\ncardSupport = CompositeCardItem(\"support-health\", supportHeader, [supportMetric, supportChart, supportList])\nsupportHeader = IconText(supportIcon, \"neutral\", \"m\", \"Support Health\", \"Ticket volume & resolution trends\", false, \"horizontal\")\nsupportIcon = Icon(\"headphones\", \"communication\")\nsupportMetric = MetricIndicatorInline(\"94.2% CSAT\", \"Customer Satisfaction Score\", { direction: \"up\", value: 2.1 })\nsupportChart = BarChart([\"Jan\", \"Feb\", \"Mar\", \"Apr\"], [openSeries, resolvedSeries], \"grouped\", \"Month\", \"Tickets\")\nopenSeries = Series(\"Opened\", [341, 327, 308, 289])\nresolvedSeries = Series(\"Resolved\", [319, 318, 301, 283])\nsupportList = EntityList([sup1, sup2, sup3], \"small\")\nsup1 = { left: \"Avg. Resolution Time\", right: \"4h 12m\", rightVariant: \"text\" }\nsup2 = { left: \"First Contact Resolution\", right: \"78.4%\", rightVariant: \"number\" }\nsup3 = { left: \"Escalation Rate\", right: \"6.2%\", rightVariant: \"number\" }\ncardMarketing = CompositeCardItem(\"marketing-roi\", marketingHeader, [marketingMetric, marketingChart, marketingList])\nmarketingHeader = IconText(marketingIcon, \"neutral\", \"m\", \"Marketing ROI\", \"Spend vs. pipeline generated\", false, \"horizontal\")\nmarketingIcon = Icon(\"megaphone\", \"communication\")\nmarketingMetric = MetricIndicatorInline(\"3.8x Blended ROI\", \"Return on Ad Spend\", { direction: \"up\", value: 0.4 })\nmarketingChart = AreaChart([\"Jan\", \"Feb\", \"Mar\", \"Apr\"], [spendSeries, pipelineSeries], \"natural\", \"Month\", \"Value ($k)\")\nspendSeries = Series(\"Ad Spend\", [44.0, 46.8, 49.2, 51.6])\npipelineSeries = Series(\"Pipeline Generated\", [158.2, 171.5, 184.9, 196.1])\nmarketingList = EntityList([mkt1, mkt2, mkt3], \"small\")\nmkt1 = { left: \"Total Ad Spend (Apr)\", right: \"$51,600\", rightVariant: \"number\" }\nmkt2 = { left: \"Cost per Lead\", right: \"$48.20\", rightVariant: \"number\" }\nmkt3 = { left: \"Top Channel\", right: \"Organic Search\", rightVariant: \"text\" }\nactionButtons = Buttons([btnChannels, btnForecast], \"row\")\nbtnChannels = Button(\"Break Down by Channel\", { type: \"continue_conversation\", context: \"Break down this month's revenue by acquisition channel\" }, \"primary\")\nbtnForecast = Button(\"Forecast Next Quarter\", { type: \"continue_conversation\", context: \"Forecast revenue and active users for next quarter\" }, \"secondary\")","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Analytics Dashboard","subtitle":"April 2026 · Business Performance Overview"},"partial":false,"hasDynamicProps":false,"statementId":"pageHeader"},{"type":"element","typeName":"SnippetCardBlock","props":{"items":[{"type":"element","typeName":"SnippetCardItem","props":{"id":"total-revenue","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"circle-dollar-sign","category":"finance"},"partial":false,"hasDynamicProps":false,"statementId":"kpi1icon"},"iconVariant":"neutral","iconSize":"m","title":"Total Revenue","subtitle":"Month to date","bold":false,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"kpi1lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"$284,500","subtext":"+12.4%","subtextVariant":"metric","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"kpi1rhs"}},"partial":false,"hasDynamicProps":false,"statementId":"kpi1"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"active-users","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"users","category":"people"},"partial":false,"hasDynamicProps":false,"statementId":"kpi2icon"},"iconVariant":"neutral","iconSize":"m","title":"Active Users","subtitle":"Monthly active users","bold":false,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"kpi2lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"47,320","subtext":"+6.8%","subtextVariant":"metric","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"kpi2rhs"}},"partial":false,"hasDynamicProps":false,"statementId":"kpi2"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"conversion-rate","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"trending-up","category":"charts"},"partial":false,"hasDynamicProps":false,"statementId":"kpi3icon"},"iconVariant":"neutral","iconSize":"m","title":"Conversion Rate","subtitle":"Avg. this month","bold":false,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"kpi3lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"5.7%","subtext":"+0.9%","subtextVariant":"metric","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"kpi3rhs"}},"partial":false,"hasDynamicProps":false,"statementId":"kpi3"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"churn-rate","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"user-minus","category":"people"},"partial":false,"hasDynamicProps":false,"statementId":"kpi4icon"},"iconVariant":"neutral","iconSize":"m","title":"Churn Rate","subtitle":"Monthly customer churn","bold":false,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"kpi4lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"2.1%","subtext":"-0.4%","subtextVariant":"metric","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"kpi4rhs"}},"partial":false,"hasDynamicProps":false,"statementId":"kpi4"}],"layout":"grid","responsive":true},"partial":false,"hasDynamicProps":false,"statementId":"kpiBlock"},{"type":"element","typeName":"SectionBlock","props":{"sections":[{"type":"element","typeName":"SectionItem","props":{"value":"users","trigger":"User Activity","content":[{"type":"element","typeName":"Tabs","props":{"items":[{"type":"element","typeName":"TabItem","props":{"value":"growth","trigger":"User Growth","content":[{"type":"element","typeName":"LineChart","props":{"labels":["May","Jun","Jul","Aug","Sep","Oct","Nov","Dec","Jan","Feb","Mar","Apr"],"series":[{"type":"element","typeName":"Series","props":{"category":"DAU","values":[5.1,5.4,5.8,6.2,6.7,7.1,7.6,8,8.4,8.9,9.3,9.8]},"partial":false,"hasDynamicProps":false,"statementId":"dauSeries"},{"type":"element","typeName":"Series","props":{"category":"MAU","values":[28.4,30.1,31.8,33.5,35.2,37.1,38.8,40.4,42.1,43.9,45.6,47.3]},"partial":false,"hasDynamicProps":false,"statementId":"mauSeries"}],"variant":"natural","xLabel":"Month","yLabel":"Users (k)"},"partial":false,"hasDynamicProps":false,"statementId":"growthChart"}]},"partial":false,"hasDynamicProps":false,"statementId":"tabGrowth"},{"type":"element","typeName":"TabItem","props":{"value":"retention","trigger":"Cohort Retention","content":[{"type":"element","typeName":"LineChart","props":{"labels":["Month 0","Month 1","Month 2","Month 3","Month 4","Month 5","Month 6"],"series":[{"type":"element","typeName":"Series","props":{"category":"Jan Cohort","values":[100,80,70,62,56,51,47]},"partial":false,"hasDynamicProps":false,"statementId":"cohortJan"},{"type":"element","typeName":"Series","props":{"category":"Feb Cohort","values":[100,83,73,65,59,54,50]},"partial":false,"hasDynamicProps":false,"statementId":"cohortFeb"},{"type":"element","typeName":"Series","props":{"category":"Mar Cohort","values":[100,86,76,68,62,0,0]},"partial":false,"hasDynamicProps":false,"statementId":"cohortMar"}],"variant":"natural","xLabel":"Month Since Signup","yLabel":"Retention (%)"},"partial":false,"hasDynamicProps":false,"statementId":"retentionChart"}]},"partial":false,"hasDynamicProps":false,"statementId":"tabRetention"}]},"partial":false,"hasDynamicProps":false,"statementId":"userTabs"}]},"partial":false,"hasDynamicProps":false,"statementId":"secUsers"},{"type":"element","typeName":"SectionItem","props":{"value":"channels","trigger":"Acquisition Channels","content":[{"type":"element","typeName":"Tabs","props":{"items":[{"type":"element","typeName":"TabItem","props":{"value":"channel-bar","trigger":"Revenue by Channel","content":[{"type":"element","typeName":"HorizontalBarChart","props":{"labels":["Organic Search","Paid Ads","Referral Program","Partner Network","Direct / Brand","Email Campaigns"],"series":[{"type":"element","typeName":"Series","props":{"category":"Revenue ($k)","values":[82.4,68.1,54.6,38.2,26.8,14.4]},"partial":false,"hasDynamicProps":false,"statementId":"channelSeries"}],"variant":"grouped","xLabel":"Revenue ($k)","yLabel":"Channel"},"partial":false,"hasDynamicProps":false,"statementId":"channelBarChart"}]},"partial":false,"hasDynamicProps":false,"statementId":"tabChannelBar"},{"type":"element","typeName":"TabItem","props":{"value":"channel-table","trigger":"Full Breakdown","content":[{"type":"element","typeName":"Table","props":{"columns":[{"type":"element","typeName":"Col","props":{"label":"Channel","data":["Organic Search","Paid Ads","Referral Program","Partner Network","Direct / Brand","Email Campaigns"],"type":"string"},"partial":false,"hasDynamicProps":false,"statementId":"colChannel"},{"type":"element","typeName":"Col","props":{"label":"Leads","data":[4210,3180,2050,1120,1290,820],"type":"number"},"partial":false,"hasDynamicProps":false,"statementId":"colLeads"},{"type":"element","typeName":"Col","props":{"label":"Conv. Rate","data":["6.4%","5.1%","9.4%","11.8%","5.8%","8.1%"],"type":"string"},"partial":false,"hasDynamicProps":false,"statementId":"colConv"},{"type":"element","typeName":"Col","props":{"label":"CAC ($)","data":[188,395,132,109,218,154],"type":"number"},"partial":false,"hasDynamicProps":false,"statementId":"colCac"},{"type":"element","typeName":"Col","props":{"label":"Revenue ($k)","data":[82.4,68.1,54.6,38.2,26.8,14.4],"type":"number"},"partial":false,"hasDynamicProps":false,"statementId":"colRevenue"},{"type":"element","typeName":"Col","props":{"label":"Status","data":["Scaling","Optimizing","Scaling","Growing","Stable","Stable"],"type":"string"},"partial":false,"hasDynamicProps":false,"statementId":"colStatus"}]},"partial":false,"hasDynamicProps":false,"statementId":"channelTable"}]},"partial":false,"hasDynamicProps":false,"statementId":"tabChannelTable"}]},"partial":false,"hasDynamicProps":false,"statementId":"channelTabs"}]},"partial":false,"hasDynamicProps":false,"statementId":"secChannels"}],"isFoldable":false},"partial":false,"hasDynamicProps":false,"statementId":"sections"},{"type":"element","typeName":"InlineHeader","props":{"heading":"Support Health & Marketing ROI","description":"Click a card to explore deeper insights"},"partial":false,"hasDynamicProps":false,"statementId":"deepDiveHeader"},{"type":"element","typeName":"CompositeCardBlock","props":{"items":[{"type":"element","typeName":"CompositeCardItem","props":{"id":"support-health","header":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"headphones","category":"communication"},"partial":false,"hasDynamicProps":false,"statementId":"supportIcon"},"iconVariant":"neutral","iconSize":"m","title":"Support Health","subtitle":"Ticket volume & resolution trends","bold":false,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"supportHeader"},"body":[{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"94.2% CSAT","subtext":"Customer Satisfaction Score","trend":{"direction":"up","value":2.1}},"partial":false,"hasDynamicProps":false,"statementId":"supportMetric"},{"type":"element","typeName":"BarChart","props":{"labels":["Jan","Feb","Mar","Apr"],"series":[{"type":"element","typeName":"Series","props":{"category":"Opened","values":[341,327,308,289]},"partial":false,"hasDynamicProps":false,"statementId":"openSeries"},{"type":"element","typeName":"Series","props":{"category":"Resolved","values":[319,318,301,283]},"partial":false,"hasDynamicProps":false,"statementId":"resolvedSeries"}],"variant":"grouped","xLabel":"Month","yLabel":"Tickets"},"partial":false,"hasDynamicProps":false,"statementId":"supportChart"},{"type":"element","typeName":"EntityList","props":{"rows":[{"left":"Avg. Resolution Time","right":"4h 12m","rightVariant":"text"},{"left":"First Contact Resolution","right":"78.4%","rightVariant":"number"},{"left":"Escalation Rate","right":"6.2%","rightVariant":"number"}],"size":"small"},"partial":false,"hasDynamicProps":false,"statementId":"supportList"}]},"partial":false,"hasDynamicProps":false,"statementId":"cardSupport"},{"type":"element","typeName":"CompositeCardItem","props":{"id":"marketing-roi","header":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"megaphone","category":"communication"},"partial":false,"hasDynamicProps":false,"statementId":"marketingIcon"},"iconVariant":"neutral","iconSize":"m","title":"Marketing ROI","subtitle":"Spend vs. pipeline generated","bold":false,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"marketingHeader"},"body":[{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"3.8x Blended ROI","subtext":"Return on Ad Spend","trend":{"direction":"up","value":0.4}},"partial":false,"hasDynamicProps":false,"statementId":"marketingMetric"},{"type":"element","typeName":"AreaChart","props":{"labels":["Jan","Feb","Mar","Apr"],"series":[{"type":"element","typeName":"Series","props":{"category":"Ad Spend","values":[44,46.8,49.2,51.6]},"partial":false,"hasDynamicProps":false,"statementId":"spendSeries"},{"type":"element","typeName":"Series","props":{"category":"Pipeline Generated","values":[158.2,171.5,184.9,196.1]},"partial":false,"hasDynamicProps":false,"statementId":"pipelineSeries"}],"variant":"natural","xLabel":"Month","yLabel":"Value ($k)"},"partial":false,"hasDynamicProps":false,"statementId":"marketingChart"},{"type":"element","typeName":"EntityList","props":{"rows":[{"left":"Total Ad Spend (Apr)","right":"$51,600","rightVariant":"number"},{"left":"Cost per Lead","right":"$48.20","rightVariant":"number"},{"left":"Top Channel","right":"Organic Search","rightVariant":"text"}],"size":"small"},"partial":false,"hasDynamicProps":false,"statementId":"marketingList"}]},"partial":false,"hasDynamicProps":false,"statementId":"cardMarketing"}],"layout":"grid","responsive":true,"action":{"type":"continue_conversation","context":"Explore this deep-dive area in more detail"}},"partial":false,"hasDynamicProps":false,"statementId":"compositeCards"},{"type":"element","typeName":"Buttons","props":{"buttons":[{"type":"element","typeName":"Button","props":{"label":"Break Down by Channel","action":{"type":"continue_conversation","context":"Break down this month's revenue by acquisition channel"},"variant":"primary"},"partial":false,"hasDynamicProps":false,"statementId":"btnChannels"},{"type":"element","typeName":"Button","props":{"label":"Forecast Next Quarter","action":{"type":"continue_conversation","context":"Forecast revenue and active users for next quarter"},"variant":"secondary"},"partial":false,"hasDynamicProps":false,"statementId":"btnForecast"}],"direction":"row"},"partial":false,"hasDynamicProps":false,"statementId":"actionButtons"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":74,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"chat-example-3","schema":"chat","input":"Example 3 — Form response (help-me-choose/plan asks: inspiration cards FIRST, then a compact form). The inspiration-card images below came from an image tool call — include such cards ONLY when you have real URLs; with no real URLs, start directly with the form:\n\nroot = Card([header, intro, inspoCards, form])\nheader = CardHeader(\"Plan Your Trip\", \"A few ideas to spark inspiration — then tell me your preferences\")\nintro = TextContent(\"Tap a style to jump straight in, or fill the quick form below for a tailored plan.\")\ninspoCards = VisualCardBlock([insp1, insp2], \"grid\", true, { type: \"continue_conversation\", context: \"Plan me a trip in this style with destinations, itinerary, and costs\" })\ninsp1 = VisualCardItem(insp1body, \"inspo-beach\", \"https://www.travelandleisure.com/thmb/cuAB7XMQ6s_Gji-no956KNopT7M=/1500x0/filters:no_upscale():max_bytes(150000):strip_icc()/TAL-maldives-TROPVACAY0325-1b5047d5931d4850b47d082966f9f563.jpg\", insp1tag, \"Maldives overwater villas\")\ninsp1body = BoldText(\"text\", \"Tropical Beach Escape\", \"Overwater villas & snorkeling\")\ninsp1tag = Tag(\"Relaxation\", insp1icon, \"sm\", \"info\")\ninsp1icon = Icon(\"umbrella\", \"travel\")\ninsp2 = VisualCardItem(insp2body, \"inspo-mountain\", \"https://www.salkantaytrekmachu.com/img/adventure-travel-peru-032.jpg\", insp2tag, \"Trekking in the Peruvian Andes\")\ninsp2body = BoldText(\"text\", \"Mountain Adventure\", \"Trekking & epic views\")\ninsp2tag = Tag(\"Active\", insp2icon, \"sm\", \"warning\")\ninsp2icon = Icon(\"mountain\", \"nature\")\nform = Form(\"trip-planner\", formButtons, [fcDestination, fcDates, fcTravellers, fcTripType, fcBudget, fcStay, fcNotes])\nformButtons = Buttons([btnSubmit, btnSkip], \"row\")\nbtnSubmit = Button(\"Plan My Trip\", { type: \"continue_conversation\", context: \"Help me plan a trip based on my form submission\" }, \"primary\")\nbtnSkip = Button(\"Just Surprise Me\", { type: \"continue_conversation\", context: \"Skip the form and surprise me with a destination and itinerary\" }, \"secondary\")\nfcDestination = FormControl(\"Destination\", destinationInput, \"Leave blank if you'd like suggestions\")\ndestinationInput = Input(\"destination\", \"e.g. Tokyo, Japan — or leave blank\", \"text\")\nfcDates = FormControl(\"Travel Dates\", datesPicker, \"Select your start and end dates\")\ndatesPicker = DatePicker(\"travel-dates\", \"range\", { required: true })\nfcTravellers = FormControl(\"Number of Travellers\", travellersSelect, \"How many people are travelling?\")\ntravellersSelect = Select(\"num-travellers\", [tr1, tr2, tr3, tr4], \"Select number of travellers\", { required: true })\ntr1 = SelectItem(\"1\", \"1 — Solo\")\ntr2 = SelectItem(\"2\", \"2 — Couple\")\ntr3 = SelectItem(\"3-4\", \"3–4 People\")\ntr4 = SelectItem(\"5+\", \"5+ People\")\nfcTripType = FormControl(\"Trip Type\", tripTypeCards, \"What kind of trip are you planning?\")\ntripTypeCards = OptionCards(\"trip-type\", \"single\", [oc1, oc2, oc3, oc4], { required: true })\noc1 = OptionCard(\"adventure\", \"Adventure\", \"Hiking, trekking & outdoor activities\", oc1icon)\noc1icon = Icon(\"mountain\", \"nature\")\noc2 = OptionCard(\"beach\", \"Beach & Relaxation\", \"Sun, sand & leisure\", oc2icon)\noc2icon = Icon(\"waves\", \"nature\")\noc3 = OptionCard(\"cultural\", \"Cultural & Historical\", \"Museums, heritage & local experiences\", oc3icon)\noc3icon = Icon(\"landmark\", \"buildings\")\noc4 = OptionCard(\"city\", \"City Break\", \"Urban exploration & nightlife\", oc4icon)\noc4icon = Icon(\"building-2\", \"buildings\")\nfcBudget = FormControl(\"Estimated Budget (per person)\", budgetSlider, \"Drag to set your per-person budget in USD\")\nbudgetSlider = Slider(\"budget\", \"discrete\", 500, 10000, 500, [2500], \"Budget (USD)\")\nfcStay = FormControl(\"Accommodation Preference\", stayChips, \"Select all that apply\")\nstayChips = Chips(\"accommodation\", \"multiple\", [ch1, ch2, ch3, ch4], {}, [\"hotel\"])\nch1 = ChipItem(\"hotel\", \"Hotel\", ch1icon)\nch1icon = Icon(\"building\", \"buildings\")\nch2 = ChipItem(\"resort\", \"Resort\", ch2icon)\nch2icon = Icon(\"umbrella\", \"travel\")\nch3 = ChipItem(\"airbnb\", \"Vacation Rental\", ch3icon)\nch3icon = Icon(\"home\", \"home\")\nch4 = ChipItem(\"camping\", \"Camping / Glamping\", ch4icon)\nch4icon = Icon(\"tent\", \"nature\")\nfcNotes = FormControl(\"Special Requests or Notes\", notesArea, \"Any dietary needs, accessibility requirements, or specific interests?\")\nnotesArea = TextArea(\"special-requests\", \"e.g. vegetarian meals, wheelchair access, honeymoon surprise...\", 4)","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Plan Your Trip","subtitle":"A few ideas to spark inspiration — then tell me your preferences"},"partial":false,"hasDynamicProps":false,"statementId":"header"},{"type":"element","typeName":"TextContent","props":{"text":"Tap a style to jump straight in, or fill the quick form below for a tailored plan."},"partial":false,"hasDynamicProps":false,"statementId":"intro"},{"type":"element","typeName":"VisualCardBlock","props":{"items":[{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Tropical Beach Escape","subtext":"Overwater villas & snorkeling","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"insp1body"},"id":"inspo-beach","bgImageSrc":"https://www.travelandleisure.com/thmb/cuAB7XMQ6s_Gji-no956KNopT7M=/1500x0/filters:no_upscale():max_bytes(150000):strip_icc()/TAL-maldives-TROPVACAY0325-1b5047d5931d4850b47d082966f9f563.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Relaxation","icon":{"type":"element","typeName":"Icon","props":{"name":"umbrella","category":"travel"},"partial":false,"hasDynamicProps":false,"statementId":"insp1icon"},"size":"sm","variant":"info"},"partial":false,"hasDynamicProps":false,"statementId":"insp1tag"},"bgImageAlt":"Maldives overwater villas"},"partial":false,"hasDynamicProps":false,"statementId":"insp1"},{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Mountain Adventure","subtext":"Trekking & epic views","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"insp2body"},"id":"inspo-mountain","bgImageSrc":"https://www.salkantaytrekmachu.com/img/adventure-travel-peru-032.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Active","icon":{"type":"element","typeName":"Icon","props":{"name":"mountain","category":"nature"},"partial":false,"hasDynamicProps":false,"statementId":"insp2icon"},"size":"sm","variant":"warning"},"partial":false,"hasDynamicProps":false,"statementId":"insp2tag"},"bgImageAlt":"Trekking in the Peruvian Andes"},"partial":false,"hasDynamicProps":false,"statementId":"insp2"}],"layout":"grid","responsive":true,"action":{"type":"continue_conversation","context":"Plan me a trip in this style with destinations, itinerary, and costs"}},"partial":false,"hasDynamicProps":false,"statementId":"inspoCards"},{"type":"element","typeName":"Form","props":{"name":"trip-planner","buttons":{"type":"element","typeName":"Buttons","props":{"buttons":[{"type":"element","typeName":"Button","props":{"label":"Plan My Trip","action":{"type":"continue_conversation","context":"Help me plan a trip based on my form submission"},"variant":"primary"},"partial":false,"hasDynamicProps":false,"statementId":"btnSubmit"},{"type":"element","typeName":"Button","props":{"label":"Just Surprise Me","action":{"type":"continue_conversation","context":"Skip the form and surprise me with a destination and itinerary"},"variant":"secondary"},"partial":false,"hasDynamicProps":false,"statementId":"btnSkip"}],"direction":"row"},"partial":false,"hasDynamicProps":false,"statementId":"formButtons"},"fields":[{"type":"element","typeName":"FormControl","props":{"label":"Destination","input":{"type":"element","typeName":"Input","props":{"name":"destination","placeholder":"e.g. Tokyo, Japan — or leave blank","type":"text"},"partial":false,"hasDynamicProps":false,"statementId":"destinationInput"},"hint":"Leave blank if you'd like suggestions"},"partial":false,"hasDynamicProps":false,"statementId":"fcDestination"},{"type":"element","typeName":"FormControl","props":{"label":"Travel Dates","input":{"type":"element","typeName":"DatePicker","props":{"name":"travel-dates","mode":"range","rules":{"required":true}},"partial":false,"hasDynamicProps":false,"statementId":"datesPicker"},"hint":"Select your start and end dates"},"partial":false,"hasDynamicProps":false,"statementId":"fcDates"},{"type":"element","typeName":"FormControl","props":{"label":"Number of Travellers","input":{"type":"element","typeName":"Select","props":{"name":"num-travellers","items":[{"type":"element","typeName":"SelectItem","props":{"value":"1","label":"1 — Solo"},"partial":false,"hasDynamicProps":false,"statementId":"tr1"},{"type":"element","typeName":"SelectItem","props":{"value":"2","label":"2 — Couple"},"partial":false,"hasDynamicProps":false,"statementId":"tr2"},{"type":"element","typeName":"SelectItem","props":{"value":"3-4","label":"3–4 People"},"partial":false,"hasDynamicProps":false,"statementId":"tr3"},{"type":"element","typeName":"SelectItem","props":{"value":"5+","label":"5+ People"},"partial":false,"hasDynamicProps":false,"statementId":"tr4"}],"placeholder":"Select number of travellers","rules":{"required":true}},"partial":false,"hasDynamicProps":false,"statementId":"travellersSelect"},"hint":"How many people are travelling?"},"partial":false,"hasDynamicProps":false,"statementId":"fcTravellers"},{"type":"element","typeName":"FormControl","props":{"label":"Trip Type","input":{"type":"element","typeName":"OptionCards","props":{"name":"trip-type","type":"single","items":[{"type":"element","typeName":"OptionCard","props":{"value":"adventure","title":"Adventure","subtitle":"Hiking, trekking & outdoor activities","topContent":{"type":"element","typeName":"Icon","props":{"name":"mountain","category":"nature"},"partial":false,"hasDynamicProps":false,"statementId":"oc1icon"}},"partial":false,"hasDynamicProps":false,"statementId":"oc1"},{"type":"element","typeName":"OptionCard","props":{"value":"beach","title":"Beach & Relaxation","subtitle":"Sun, sand & leisure","topContent":{"type":"element","typeName":"Icon","props":{"name":"waves","category":"nature"},"partial":false,"hasDynamicProps":false,"statementId":"oc2icon"}},"partial":false,"hasDynamicProps":false,"statementId":"oc2"},{"type":"element","typeName":"OptionCard","props":{"value":"cultural","title":"Cultural & Historical","subtitle":"Museums, heritage & local experiences","topContent":{"type":"element","typeName":"Icon","props":{"name":"landmark","category":"buildings"},"partial":false,"hasDynamicProps":false,"statementId":"oc3icon"}},"partial":false,"hasDynamicProps":false,"statementId":"oc3"},{"type":"element","typeName":"OptionCard","props":{"value":"city","title":"City Break","subtitle":"Urban exploration & nightlife","topContent":{"type":"element","typeName":"Icon","props":{"name":"building-2","category":"buildings"},"partial":false,"hasDynamicProps":false,"statementId":"oc4icon"}},"partial":false,"hasDynamicProps":false,"statementId":"oc4"}],"rules":{"required":true}},"partial":false,"hasDynamicProps":false,"statementId":"tripTypeCards"},"hint":"What kind of trip are you planning?"},"partial":false,"hasDynamicProps":false,"statementId":"fcTripType"},{"type":"element","typeName":"FormControl","props":{"label":"Estimated Budget (per person)","input":{"type":"element","typeName":"Slider","props":{"name":"budget","variant":"discrete","min":500,"max":10000,"step":500,"defaultValue":[2500],"label":"Budget (USD)"},"partial":false,"hasDynamicProps":false,"statementId":"budgetSlider"},"hint":"Drag to set your per-person budget in USD"},"partial":false,"hasDynamicProps":false,"statementId":"fcBudget"},{"type":"element","typeName":"FormControl","props":{"label":"Accommodation Preference","input":{"type":"element","typeName":"Chips","props":{"name":"accommodation","type":"multiple","items":[{"type":"element","typeName":"ChipItem","props":{"value":"hotel","label":"Hotel","icon":{"type":"element","typeName":"Icon","props":{"name":"building","category":"buildings"},"partial":false,"hasDynamicProps":false,"statementId":"ch1icon"}},"partial":false,"hasDynamicProps":false,"statementId":"ch1"},{"type":"element","typeName":"ChipItem","props":{"value":"resort","label":"Resort","icon":{"type":"element","typeName":"Icon","props":{"name":"umbrella","category":"travel"},"partial":false,"hasDynamicProps":false,"statementId":"ch2icon"}},"partial":false,"hasDynamicProps":false,"statementId":"ch2"},{"type":"element","typeName":"ChipItem","props":{"value":"airbnb","label":"Vacation Rental","icon":{"type":"element","typeName":"Icon","props":{"name":"home","category":"home"},"partial":false,"hasDynamicProps":false,"statementId":"ch3icon"}},"partial":false,"hasDynamicProps":false,"statementId":"ch3"},{"type":"element","typeName":"ChipItem","props":{"value":"camping","label":"Camping / Glamping","icon":{"type":"element","typeName":"Icon","props":{"name":"tent","category":"nature"},"partial":false,"hasDynamicProps":false,"statementId":"ch4icon"}},"partial":false,"hasDynamicProps":false,"statementId":"ch4"}],"rules":{},"defaultValue":["hotel"]},"partial":false,"hasDynamicProps":false,"statementId":"stayChips"},"hint":"Select all that apply"},"partial":false,"hasDynamicProps":false,"statementId":"fcStay"},{"type":"element","typeName":"FormControl","props":{"label":"Special Requests or Notes","input":{"type":"element","typeName":"TextArea","props":{"name":"special-requests","placeholder":"e.g. vegetarian meals, wheelchair access, honeymoon surprise...","rows":4},"partial":false,"hasDynamicProps":false,"statementId":"notesArea"},"hint":"Any dietary needs, accessibility requirements, or specific interests?"},"partial":false,"hasDynamicProps":false,"statementId":"fcNotes"}]},"partial":false,"hasDynamicProps":false,"statementId":"form"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":50,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"chat-example-4","schema":"chat","input":"Example 4 — Recommendations with images and links (same pattern for products, movies, restaurants, articles):\n\nroot = Card([header, intro, posters, linksHeader, trackCards], [src1, src2])\nheader = CardHeader(\"Retro-Energetic Indian Tracks\", \"New Hindi releases with 80s/90s dance DNA\")\nintro = TextContent(\"These recent tracks channel retro synths and 90s party percussion [1][2]. Tap a poster to explore a track, or use its button to watch the exact official video.\")\nposters = VisualCardBlock([v1, v2], \"grid\", true, { type: \"continue_conversation\", context: \"Tell me more about this track and its retro influences\" })\nv1 = VisualCardItem(v1body, \"tamma\", \"https://i.ytimg.com/vi/PQHeOb0Q9oo/hq720.jpg\", v1tag, \"Tamma Tamma video poster\")\nv1body = BoldText(\"text\", \"Tamma Tamma\", \"Dhurandhar · Ranveer Singh\")\nv1tag = Tag(\"80s Electro-Disco\", v1icon, \"sm\", \"success\")\nv1icon = Icon(\"disc-3\", \"multimedia\")\nv2 = VisualCardItem(v2body, \"sajan\", \"https://i.ytimg.com/vi/mLz-qBiRnl8/hq720.jpg\", v2tag, \"Sajan Re video poster\")\nv2body = BoldText(\"text\", \"Sajan Re\", \"Nora Fatehi · Badshah\")\nv2tag = Tag(\"Synthwave Electro\", v2icon, \"sm\", \"info\")\nv2icon = Icon(\"music\", \"multimedia\")\nlinksHeader = InlineHeader(\"Watch the Exact Videos\", \"Each button opens that track's official video\")\ntrackCards = CompositeCardBlock([t1, t2], \"grid\", true)\nt1 = CompositeCardItem(\"t-tamma\", t1h, [t1body, t1tags], { button: t1btn })\nt1h = IconText(t1icon, \"neutral\", \"m\", \"Tamma Tamma\", \"Bappi Lahiri classic, electro-funk rework\", true, \"horizontal\")\nt1icon = Icon(\"disc-3\", \"multimedia\")\nt1body = Text(\"text\", \"Heavy analog synths and brass over a fast disco groove.\", \"Retro Electro-Disco\")\nt1tags = TagBlock([\"2026 Film Hit\", \"Peak Party\"])\nt1btn = Button(\"Watch on YouTube\", { type: \"open_url\", url: \"https://www.youtube.com/watch?v=PQHeOb0Q9oo\" }, \"primary\")\nt2 = CompositeCardItem(\"t-sajan\", t2h, [t2body, t2tags], { button: t2btn })\nt2h = IconText(t2icon, \"neutral\", \"m\", \"Sajan Re\", \"Nora Fatehi · Badshah · Sanjoy\", true, \"horizontal\")\nt2icon = Icon(\"music\", \"multimedia\")\nt2body = Text(\"text\", \"Synthwave melody hooks over a modern club beat.\", \"Retro-Fusion Electro\")\nt2tags = TagBlock([\"Club Anthem\", \"Nora x Badshah\"])\nt2btn = Button(\"Watch on YouTube\", { type: \"open_url\", url: \"https://www.youtube.com/watch?v=COUSCnFazzc\" }, \"primary\")\nsrc1 = { title: \"Tamma Tamma (Full Video) - Dhurandhar\", sourceName: \"YouTube (T-Series)\", url: \"https://www.youtube.com/watch?v=PQHeOb0Q9oo\" }\nsrc2 = { title: \"Sajan Re (Official Music Video)\", sourceName: \"YouTube (T-Series)\", url: \"https://www.youtube.com/watch?v=COUSCnFazzc\" }","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Retro-Energetic Indian Tracks","subtitle":"New Hindi releases with 80s/90s dance DNA"},"partial":false,"hasDynamicProps":false,"statementId":"header"},{"type":"element","typeName":"TextContent","props":{"text":"These recent tracks channel retro synths and 90s party percussion [1][2]. Tap a poster to explore a track, or use its button to watch the exact official video."},"partial":false,"hasDynamicProps":false,"statementId":"intro"},{"type":"element","typeName":"VisualCardBlock","props":{"items":[{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Tamma Tamma","subtext":"Dhurandhar · Ranveer Singh","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"v1body"},"id":"tamma","bgImageSrc":"https://i.ytimg.com/vi/PQHeOb0Q9oo/hq720.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"80s Electro-Disco","icon":{"type":"element","typeName":"Icon","props":{"name":"disc-3","category":"multimedia"},"partial":false,"hasDynamicProps":false,"statementId":"v1icon"},"size":"sm","variant":"success"},"partial":false,"hasDynamicProps":false,"statementId":"v1tag"},"bgImageAlt":"Tamma Tamma video poster"},"partial":false,"hasDynamicProps":false,"statementId":"v1"},{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Sajan Re","subtext":"Nora Fatehi · Badshah","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"v2body"},"id":"sajan","bgImageSrc":"https://i.ytimg.com/vi/mLz-qBiRnl8/hq720.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Synthwave Electro","icon":{"type":"element","typeName":"Icon","props":{"name":"music","category":"multimedia"},"partial":false,"hasDynamicProps":false,"statementId":"v2icon"},"size":"sm","variant":"info"},"partial":false,"hasDynamicProps":false,"statementId":"v2tag"},"bgImageAlt":"Sajan Re video poster"},"partial":false,"hasDynamicProps":false,"statementId":"v2"}],"layout":"grid","responsive":true,"action":{"type":"continue_conversation","context":"Tell me more about this track and its retro influences"}},"partial":false,"hasDynamicProps":false,"statementId":"posters"},{"type":"element","typeName":"InlineHeader","props":{"heading":"Watch the Exact Videos","description":"Each button opens that track's official video"},"partial":false,"hasDynamicProps":false,"statementId":"linksHeader"},{"type":"element","typeName":"CompositeCardBlock","props":{"items":[{"type":"element","typeName":"CompositeCardItem","props":{"id":"t-tamma","header":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"disc-3","category":"multimedia"},"partial":false,"hasDynamicProps":false,"statementId":"t1icon"},"iconVariant":"neutral","iconSize":"m","title":"Tamma Tamma","subtitle":"Bappi Lahiri classic, electro-funk rework","bold":true,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"t1h"},"body":[{"type":"element","typeName":"Text","props":{"variant":"text","value":"Heavy analog synths and brass over a fast disco groove.","subtext":"Retro Electro-Disco","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"t1body"},{"type":"element","typeName":"TagBlock","props":{"tags":["2026 Film Hit","Peak Party"]},"partial":false,"hasDynamicProps":false,"statementId":"t1tags"}],"footer":{"button":{"type":"element","typeName":"Button","props":{"label":"Watch on YouTube","action":{"type":"open_url","url":"https://www.youtube.com/watch?v=PQHeOb0Q9oo"},"variant":"primary"},"partial":false,"hasDynamicProps":false,"statementId":"t1btn"}}},"partial":false,"hasDynamicProps":false,"statementId":"t1"},{"type":"element","typeName":"CompositeCardItem","props":{"id":"t-sajan","header":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"music","category":"multimedia"},"partial":false,"hasDynamicProps":false,"statementId":"t2icon"},"iconVariant":"neutral","iconSize":"m","title":"Sajan Re","subtitle":"Nora Fatehi · Badshah · Sanjoy","bold":true,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"t2h"},"body":[{"type":"element","typeName":"Text","props":{"variant":"text","value":"Synthwave melody hooks over a modern club beat.","subtext":"Retro-Fusion Electro","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"t2body"},{"type":"element","typeName":"TagBlock","props":{"tags":["Club Anthem","Nora x Badshah"]},"partial":false,"hasDynamicProps":false,"statementId":"t2tags"}],"footer":{"button":{"type":"element","typeName":"Button","props":{"label":"Watch on YouTube","action":{"type":"open_url","url":"https://www.youtube.com/watch?v=COUSCnFazzc"},"variant":"primary"},"partial":false,"hasDynamicProps":false,"statementId":"t2btn"}}},"partial":false,"hasDynamicProps":false,"statementId":"t2"}],"layout":"grid","responsive":true},"partial":false,"hasDynamicProps":false,"statementId":"trackCards"}],"sources":[{"title":"Tamma Tamma (Full Video) - Dhurandhar","sourceName":"YouTube (T-Series)","url":"https://www.youtube.com/watch?v=PQHeOb0Q9oo"},{"title":"Sajan Re (Official Music Video)","sourceName":"YouTube (T-Series)","url":"https://www.youtube.com/watch?v=COUSCnFazzc"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":28,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"chat-example-5","schema":"chat","input":"Example 5 — Table with follow-ups:\n\nroot = Card([title, tbl, followUps])\ntitle = TextContent(\"Top Languages\", \"large-heavy\")\ntbl = Table([Col(\"Language\", langs), Col(\"Users (M)\", users), Col(\"Year\", years)])\nlangs = [\"Python\", \"JavaScript\", \"Java\"]\nusers = [15.7, 14.2, 12.1]\nyears = [1991, 1995, 1995]\nfollowUps = FollowUpBlock([fu1, fu2])\nfu1 = FollowUpItem(\"Tell me more about Python\")\nfu2 = FollowUpItem(\"Show me a JavaScript comparison\")","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"Top Languages","size":"large-heavy"},"partial":false,"hasDynamicProps":false,"statementId":"title"},{"type":"element","typeName":"Table","props":{"columns":[{"type":"element","typeName":"Col","props":{"label":"Language","data":["Python","JavaScript","Java"]},"partial":false,"hasDynamicProps":false},{"type":"element","typeName":"Col","props":{"label":"Users (M)","data":[15.7,14.2,12.1]},"partial":false,"hasDynamicProps":false},{"type":"element","typeName":"Col","props":{"label":"Year","data":[1991,1995,1995]},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"tbl"},{"type":"element","typeName":"FollowUpBlock","props":{"items":[{"type":"element","typeName":"FollowUpItem","props":{"text":"Tell me more about Python"},"partial":false,"hasDynamicProps":false,"statementId":"fu1"},{"type":"element","typeName":"FollowUpItem","props":{"text":"Show me a JavaScript comparison"},"partial":false,"hasDynamicProps":false,"statementId":"fu2"}]},"partial":false,"hasDynamicProps":false,"statementId":"followUps"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":9,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"openui-example-1","schema":"openui","input":"Example 1 — Table (column-oriented):\n\nroot = Stack([title, tbl])\ntitle = TextContent(\"Top Languages\", \"large-heavy\")\ntbl = Table([Col(\"Language\", langs), Col(\"Users (M)\", users), Col(\"Year\", years)])\nlangs = [\"Python\", \"JavaScript\", \"Java\", \"TypeScript\", \"Go\"]\nusers = [15.7, 14.2, 12.1, 8.5, 5.2]\nyears = [1991, 1995, 1995, 2012, 2009]","expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"Top Languages","size":"large-heavy"},"partial":false,"hasDynamicProps":false,"statementId":"title"},{"type":"element","typeName":"Table","props":{"columns":[{"type":"element","typeName":"Col","props":{"label":"Language","data":["Python","JavaScript","Java","TypeScript","Go"]},"partial":false,"hasDynamicProps":false},{"type":"element","typeName":"Col","props":{"label":"Users (M)","data":[15.7,14.2,12.1,8.5,5.2]},"partial":false,"hasDynamicProps":false},{"type":"element","typeName":"Col","props":{"label":"Year","data":[1991,1995,1995,2012,2009]},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"tbl"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":6,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"openui-example-2","schema":"openui","input":"Example 2 — Bar chart:\n\nroot = Stack([title, chart])\ntitle = TextContent(\"Q4 Revenue\", \"large-heavy\")\nchart = BarChart(labels, [s1, s2], \"grouped\")\nlabels = [\"Oct\", \"Nov\", \"Dec\"]\ns1 = Series(\"Product A\", [120, 150, 180])\ns2 = Series(\"Product B\", [90, 110, 140])","expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"Q4 Revenue","size":"large-heavy"},"partial":false,"hasDynamicProps":false,"statementId":"title"},{"type":"element","typeName":"BarChart","props":{"labels":["Oct","Nov","Dec"],"series":[{"type":"element","typeName":"Series","props":{"category":"Product A","values":[120,150,180]},"partial":false,"hasDynamicProps":false,"statementId":"s1"},{"type":"element","typeName":"Series","props":{"category":"Product B","values":[90,110,140]},"partial":false,"hasDynamicProps":false,"statementId":"s2"}],"variant":"grouped"},"partial":false,"hasDynamicProps":false,"statementId":"chart"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":6,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"openui-example-3","schema":"openui","input":"Example 3 — Form with validation:\n\nroot = Stack([title, form])\ntitle = TextContent(\"Contact Us\", \"large-heavy\")\nform = Form(\"contact\", btns, [nameField, emailField, countryField, msgField])\nnameField = FormControl(\"Name\", Input(\"name\", \"Your name\", \"text\", { required: true, minLength: 2 }))\nemailField = FormControl(\"Email\", Input(\"email\", \"you@example.com\", \"email\", { required: true, email: true }))\ncountryField = FormControl(\"Country\", Select(\"country\", countryOpts, \"Select...\", { required: true }))\nmsgField = FormControl(\"Message\", TextArea(\"message\", \"Tell us more...\", 4, { required: true, minLength: 10 }))\ncountryOpts = [SelectItem(\"us\", \"United States\"), SelectItem(\"uk\", \"United Kingdom\"), SelectItem(\"de\", \"Germany\")]\nbtns = Buttons([Button(\"Submit\", Action([@ToAssistant(\"Submit\")]), \"primary\"), Button(\"Cancel\", Action([@ToAssistant(\"Cancel\")]), \"secondary\")])","expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"Contact Us","size":"large-heavy"},"partial":false,"hasDynamicProps":false,"statementId":"title"},{"type":"element","typeName":"Form","props":{"name":"contact","buttons":{"type":"element","typeName":"Buttons","props":{"buttons":[{"type":"element","typeName":"Button","props":{"label":"Submit","action":{"k":"Comp","name":"Action","args":[{"k":"Arr","els":[{"k":"Comp","name":"ToAssistant","args":[{"k":"Str","v":"Submit"}]}]}]},"variant":"primary"},"partial":false,"hasDynamicProps":true},{"type":"element","typeName":"Button","props":{"label":"Cancel","action":{"k":"Comp","name":"Action","args":[{"k":"Arr","els":[{"k":"Comp","name":"ToAssistant","args":[{"k":"Str","v":"Cancel"}]}]}]},"variant":"secondary"},"partial":false,"hasDynamicProps":true}]},"partial":false,"hasDynamicProps":true,"statementId":"btns"},"fields":[{"type":"element","typeName":"FormControl","props":{"label":"Name","input":{"type":"element","typeName":"Input","props":{"name":"name","placeholder":"Your name","type":"text","rules":{"required":true,"minLength":2}},"partial":false,"hasDynamicProps":false}},"partial":false,"hasDynamicProps":false,"statementId":"nameField"},{"type":"element","typeName":"FormControl","props":{"label":"Email","input":{"type":"element","typeName":"Input","props":{"name":"email","placeholder":"you@example.com","type":"email","rules":{"required":true,"email":true}},"partial":false,"hasDynamicProps":false}},"partial":false,"hasDynamicProps":false,"statementId":"emailField"},{"type":"element","typeName":"FormControl","props":{"label":"Country","input":{"type":"element","typeName":"Select","props":{"name":"country","items":[{"type":"element","typeName":"SelectItem","props":{"value":"us","label":"United States"},"partial":false,"hasDynamicProps":false},{"type":"element","typeName":"SelectItem","props":{"value":"uk","label":"United Kingdom"},"partial":false,"hasDynamicProps":false},{"type":"element","typeName":"SelectItem","props":{"value":"de","label":"Germany"},"partial":false,"hasDynamicProps":false}],"placeholder":"Select...","rules":{"required":true}},"partial":false,"hasDynamicProps":false}},"partial":false,"hasDynamicProps":false,"statementId":"countryField"},{"type":"element","typeName":"FormControl","props":{"label":"Message","input":{"type":"element","typeName":"TextArea","props":{"name":"message","placeholder":"Tell us more...","rows":4,"rules":{"required":true,"minLength":10}},"partial":false,"hasDynamicProps":false}},"partial":false,"hasDynamicProps":false,"statementId":"msgField"}]},"partial":false,"hasDynamicProps":true,"statementId":"form"}]},"partial":false,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":9,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"openui-example-4","schema":"openui","input":"Example 4 — Tabs with mixed content:\n\nroot = Stack([title, tabs])\ntitle = TextContent(\"React vs Vue\", \"large-heavy\")\ntabs = Tabs([tabReact, tabVue])\ntabReact = TabItem(\"react\", \"React\", reactContent)\ntabVue = TabItem(\"vue\", \"Vue\", vueContent)\nreactContent = [TextContent(\"React is a library by Meta for building UIs.\"), Callout(\"info\", \"Note\", \"React uses JSX syntax.\")]\nvueContent = [TextContent(\"Vue is a progressive framework by Evan You.\"), Callout(\"success\", \"Tip\", \"Vue has a gentle learning curve.\")]","expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"React vs Vue","size":"large-heavy"},"partial":false,"hasDynamicProps":false,"statementId":"title"},{"type":"element","typeName":"Tabs","props":{"items":[{"type":"element","typeName":"TabItem","props":{"value":"react","trigger":"React","content":[{"type":"element","typeName":"TextContent","props":{"text":"React is a library by Meta for building UIs."},"partial":false,"hasDynamicProps":false},{"type":"element","typeName":"Callout","props":{"variant":"info","title":"Note","description":"React uses JSX syntax."},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"tabReact"},{"type":"element","typeName":"TabItem","props":{"value":"vue","trigger":"Vue","content":[{"type":"element","typeName":"TextContent","props":{"text":"Vue is a progressive framework by Evan You."},"partial":false,"hasDynamicProps":false},{"type":"element","typeName":"Callout","props":{"variant":"success","title":"Tip","description":"Vue has a gentle learning curve."},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"tabVue"}]},"partial":false,"hasDynamicProps":false,"statementId":"tabs"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":7,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"openui-example-5","schema":"openui","input":"Example 5 — KPI and product cards:\n\nroot = Stack([kpiHeader, kpiBlock, productsHeader, productCards])\nkpiHeader = InlineHeader(\"This Month\", \"Key account metrics\")\nkpiBlock = SnippetCardBlock([kpi1, kpi2, kpi3])\nkpi1 = SnippetCardItem(\"revenue\", kpi1lhs, kpi1rhs)\nkpi1lhs = IconText(kpi1icon, \"neutral\", \"m\", \"Revenue\", \"Month to date\", false, \"horizontal\")\nkpi1icon = Icon(\"circle-dollar-sign\", \"finance\")\nkpi1rhs = BoldText(\"number\", \"$48,200\", \"+8.1%\", \"metric\")\nkpi2 = SnippetCardItem(\"active-users\", kpi2lhs, kpi2rhs)\nkpi2lhs = IconText(kpi2icon, \"neutral\", \"m\", \"Active Users\", \"This month\", false, \"horizontal\")\nkpi2icon = Icon(\"users\", \"people\")\nkpi2rhs = BoldText(\"number\", \"1,204\", \"+3.4%\", \"metric\")\nkpi3 = SnippetCardItem(\"churn\", kpi3lhs, kpi3rhs)\nkpi3lhs = IconText(kpi3icon, \"neutral\", \"m\", \"Churn\", \"This month\", false, \"horizontal\")\nkpi3icon = Icon(\"user-minus\", \"people\")\nkpi3rhs = BoldText(\"number\", \"1.8%\", \"-0.3%\", \"metric\")\nproductsHeader = InlineHeader(\"Top Products\", \"By units sold\")\nproductCards = CompositeCardBlock([p1, p2])\np1 = CompositeCardItem(\"widget-pro\", p1header, [p1body], p1footer)\np1header = IconText(p1icon, \"neutral\", \"m\", \"Widget Pro\", \"Best seller\", true, \"horizontal\")\np1icon = Icon(\"package\", \"shopping\")\np1body = MetricIndicatorInline(\"2,410 units\", \"Sold this month\", { direction: \"up\", value: 12 })\np1footer = { price: BoldText(\"text\", \"$29.00\"), button: p1btn }\np1btn = Button(\"View Details\", Action([@ToAssistant(\"Show details for Widget Pro\")]), \"secondary\")\np2 = CompositeCardItem(\"widget-mini\", p2header, [p2body], p2footer)\np2header = IconText(p2icon, \"neutral\", \"m\", \"Widget Mini\", \"New arrival\", true, \"horizontal\")\np2icon = Icon(\"box\", \"shopping\")\np2body = MetricIndicatorInline(\"980 units\", \"Sold this month\", { direction: \"up\", value: 4 })\np2footer = { price: BoldText(\"text\", \"$14.00\"), button: p2btn }\np2btn = Button(\"View Details\", Action([@ToAssistant(\"Show details for Widget Mini\")]), \"secondary\")","expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"InlineHeader","props":{"heading":"This Month","description":"Key account metrics"},"partial":false,"hasDynamicProps":false,"statementId":"kpiHeader"},{"type":"element","typeName":"SnippetCardBlock","props":{"items":[{"type":"element","typeName":"SnippetCardItem","props":{"id":"revenue","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"circle-dollar-sign","category":"finance"},"partial":false,"hasDynamicProps":false,"statementId":"kpi1icon"},"iconVariant":"neutral","iconSize":"m","title":"Revenue","subtitle":"Month to date","bold":false,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"kpi1lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"$48,200","subtext":"+8.1%","subtextVariant":"metric","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"kpi1rhs"}},"partial":false,"hasDynamicProps":false,"statementId":"kpi1"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"active-users","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"users","category":"people"},"partial":false,"hasDynamicProps":false,"statementId":"kpi2icon"},"iconVariant":"neutral","iconSize":"m","title":"Active Users","subtitle":"This month","bold":false,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"kpi2lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"1,204","subtext":"+3.4%","subtextVariant":"metric","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"kpi2rhs"}},"partial":false,"hasDynamicProps":false,"statementId":"kpi2"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"churn","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"user-minus","category":"people"},"partial":false,"hasDynamicProps":false,"statementId":"kpi3icon"},"iconVariant":"neutral","iconSize":"m","title":"Churn","subtitle":"This month","bold":false,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"kpi3lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"1.8%","subtext":"-0.3%","subtextVariant":"metric","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"kpi3rhs"}},"partial":false,"hasDynamicProps":false,"statementId":"kpi3"}],"layout":"grid","responsive":true},"partial":false,"hasDynamicProps":false,"statementId":"kpiBlock"},{"type":"element","typeName":"InlineHeader","props":{"heading":"Top Products","description":"By units sold"},"partial":false,"hasDynamicProps":false,"statementId":"productsHeader"},{"type":"element","typeName":"CompositeCardBlock","props":{"items":[{"type":"element","typeName":"CompositeCardItem","props":{"id":"widget-pro","header":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"package","category":"shopping"},"partial":false,"hasDynamicProps":false,"statementId":"p1icon"},"iconVariant":"neutral","iconSize":"m","title":"Widget Pro","subtitle":"Best seller","bold":true,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"p1header"},"body":[{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"2,410 units","subtext":"Sold this month","trend":{"direction":"up","value":12}},"partial":false,"hasDynamicProps":false,"statementId":"p1body"}],"footer":{"price":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"$29.00","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false},"button":{"type":"element","typeName":"Button","props":{"label":"View Details","action":{"k":"Comp","name":"Action","args":[{"k":"Arr","els":[{"k":"Comp","name":"ToAssistant","args":[{"k":"Str","v":"Show details for Widget Pro"}]}]}]},"variant":"secondary"},"partial":false,"hasDynamicProps":true,"statementId":"p1btn"}}},"partial":false,"hasDynamicProps":true,"statementId":"p1"},{"type":"element","typeName":"CompositeCardItem","props":{"id":"widget-mini","header":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"box","category":"shopping"},"partial":false,"hasDynamicProps":false,"statementId":"p2icon"},"iconVariant":"neutral","iconSize":"m","title":"Widget Mini","subtitle":"New arrival","bold":true,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"p2header"},"body":[{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"980 units","subtext":"Sold this month","trend":{"direction":"up","value":4}},"partial":false,"hasDynamicProps":false,"statementId":"p2body"}],"footer":{"price":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"$14.00","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false},"button":{"type":"element","typeName":"Button","props":{"label":"View Details","action":{"k":"Comp","name":"Action","args":[{"k":"Arr","els":[{"k":"Comp","name":"ToAssistant","args":[{"k":"Str","v":"Show details for Widget Mini"}]}]}]},"variant":"secondary"},"partial":false,"hasDynamicProps":true,"statementId":"p2btn"}}},"partial":false,"hasDynamicProps":true,"statementId":"p2"}],"layout":"grid","responsive":true},"partial":false,"hasDynamicProps":true,"statementId":"productCards"}]},"partial":false,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":29,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"openui-example-6","schema":"openui","input":"Example 6 — Editable table and a selection form:\n\nroot = Stack([tableHeader, editTable, formHeader, prefsForm])\ntableHeader = InlineHeader(\"Team Roster\", \"Click a cell to edit\")\neditTable = EditableTable(\"roster\", [colName, colRole, colStart], [row1, row2])\ncolName = { type: \"text\", key: \"name\", header: \"Name\" }\ncolRole = { type: \"select\", key: \"role\", header: \"Role\", options: [roleEng, roleDesign] }\nroleEng = { value: \"eng\", label: \"Engineering\" }\nroleDesign = { value: \"design\", label: \"Design\" }\ncolStart = { type: \"date-single\", key: \"start\", header: \"Start Date\" }\nrow1 = { id: \"1\", values: [\"Alex Kim\", \"eng\", \"2024-01-15\"] }\nrow2 = { id: \"2\", values: [\"Jamie Lee\", \"design\", \"2024-03-02\"] }\nformHeader = InlineHeader(\"Preferences\", \"Tell us how you like to work\")\nprefsForm = Form(\"prefs\", formButtons, [fc1, fc2])\nfc1 = FormControl(\"Working Style\", styleCards, \"Pick the one that fits best\")\nstyleCards = OptionCards(\"style\", \"single\", [style1, style2])\nstyle1 = OptionCard(\"focused\", \"Deep Focus\", \"Long uninterrupted blocks\", style1icon)\nstyle1icon = Icon(\"target\", \"tools\")\nstyle2 = OptionCard(\"collab\", \"Collaborative\", \"Frequent pairing and syncs\", style2icon)\nstyle2icon = Icon(\"users\", \"people\")\nfc2 = FormControl(\"Tools\", toolChips, \"Select all that apply\")\ntoolChips = Chips(\"tools\", \"multiple\", [tool1, tool2, tool3])\ntool1 = ChipItem(\"figma\", \"Figma\", tool1icon)\ntool1icon = Icon(\"figma\", \"design\")\ntool2 = ChipItem(\"slack\", \"Slack\", tool2icon)\ntool2icon = Icon(\"message-square\", \"communication\")\ntool3 = ChipItem(\"notion\", \"Notion\", tool3icon)\ntool3icon = Icon(\"notebook\", \"text\")\nformButtons = Buttons([Button(\"Save\", Action([@ToAssistant(\"Save preferences\")]), \"primary\")])","expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"InlineHeader","props":{"heading":"Team Roster","description":"Click a cell to edit"},"partial":false,"hasDynamicProps":false,"statementId":"tableHeader"},{"type":"element","typeName":"EditableTable","props":{"name":"roster","columns":[{"type":"text","key":"name","header":"Name"},{"type":"select","key":"role","header":"Role","options":[{"value":"eng","label":"Engineering"},{"value":"design","label":"Design"}]},{"type":"date-single","key":"start","header":"Start Date"}],"data":[{"id":"1","values":["Alex Kim","eng","2024-01-15"]},{"id":"2","values":["Jamie Lee","design","2024-03-02"]}]},"partial":false,"hasDynamicProps":false,"statementId":"editTable"},{"type":"element","typeName":"InlineHeader","props":{"heading":"Preferences","description":"Tell us how you like to work"},"partial":false,"hasDynamicProps":false,"statementId":"formHeader"},{"type":"element","typeName":"Form","props":{"name":"prefs","buttons":{"type":"element","typeName":"Buttons","props":{"buttons":[{"type":"element","typeName":"Button","props":{"label":"Save","action":{"k":"Comp","name":"Action","args":[{"k":"Arr","els":[{"k":"Comp","name":"ToAssistant","args":[{"k":"Str","v":"Save preferences"}]}]}]},"variant":"primary"},"partial":false,"hasDynamicProps":true}]},"partial":false,"hasDynamicProps":true,"statementId":"formButtons"},"fields":[{"type":"element","typeName":"FormControl","props":{"label":"Working Style","input":{"type":"element","typeName":"OptionCards","props":{"name":"style","type":"single","items":[{"type":"element","typeName":"OptionCard","props":{"value":"focused","title":"Deep Focus","subtitle":"Long uninterrupted blocks","topContent":{"type":"element","typeName":"Icon","props":{"name":"target","category":"tools"},"partial":false,"hasDynamicProps":false,"statementId":"style1icon"}},"partial":false,"hasDynamicProps":false,"statementId":"style1"},{"type":"element","typeName":"OptionCard","props":{"value":"collab","title":"Collaborative","subtitle":"Frequent pairing and syncs","topContent":{"type":"element","typeName":"Icon","props":{"name":"users","category":"people"},"partial":false,"hasDynamicProps":false,"statementId":"style2icon"}},"partial":false,"hasDynamicProps":false,"statementId":"style2"}]},"partial":false,"hasDynamicProps":false,"statementId":"styleCards"},"hint":"Pick the one that fits best"},"partial":false,"hasDynamicProps":false,"statementId":"fc1"},{"type":"element","typeName":"FormControl","props":{"label":"Tools","input":{"type":"element","typeName":"Chips","props":{"name":"tools","type":"multiple","items":[{"type":"element","typeName":"ChipItem","props":{"value":"figma","label":"Figma","icon":{"type":"element","typeName":"Icon","props":{"name":"figma","category":"design"},"partial":false,"hasDynamicProps":false,"statementId":"tool1icon"}},"partial":false,"hasDynamicProps":false,"statementId":"tool1"},{"type":"element","typeName":"ChipItem","props":{"value":"slack","label":"Slack","icon":{"type":"element","typeName":"Icon","props":{"name":"message-square","category":"communication"},"partial":false,"hasDynamicProps":false,"statementId":"tool2icon"}},"partial":false,"hasDynamicProps":false,"statementId":"tool2"},{"type":"element","typeName":"ChipItem","props":{"value":"notion","label":"Notion","icon":{"type":"element","typeName":"Icon","props":{"name":"notebook","category":"text"},"partial":false,"hasDynamicProps":false,"statementId":"tool3icon"}},"partial":false,"hasDynamicProps":false,"statementId":"tool3"}]},"partial":false,"hasDynamicProps":false,"statementId":"toolChips"},"hint":"Select all that apply"},"partial":false,"hasDynamicProps":false,"statementId":"fc2"}]},"partial":false,"hasDynamicProps":true,"statementId":"prefsForm"}]},"partial":false,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":27,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"basic","schema":"test","input":"root = Card([t1, b1])\nt1 = Text(\"Hi\")\nb1 = Button(\"Go\")","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"Hi"},"partial":false,"hasDynamicProps":false,"statementId":"t1"},{"type":"element","typeName":"Button","props":{"label":"Go"},"partial":false,"hasDynamicProps":false,"statementId":"b1"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":3,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"missing-required","schema":"test","input":"root = Card([t1, b1])\nt1 = Text()\nb1 = Button(\"Go\")","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Button","props":{"label":"Go"},"partial":false,"hasDynamicProps":false,"statementId":"b1"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":3,"errors":[{"code":"missing-required","component":"Text","path":"/text","message":"missing required field \"/text\" — signature: Text(text*: string, size: \"s\"|\"m\"|\"l\")","statementId":"t1"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"null-required-with-default","schema":"test","input":"root = Card([t1])\nt1 = Text(null)","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[{"code":"null-required","component":"Text","path":"/text","message":"required field \"/text\" cannot be null — signature: Text(text*: string, size: \"s\"|\"m\"|\"l\")","statementId":"t1"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"required-default-fills","schema":"test","input":"root = Card([x])\nx = Tagged(1, true, \"x\", null)","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Tagged","props":{"count":1,"flag":true,"kind":"x","tone":"neutral"},"partial":false,"hasDynamicProps":false,"statementId":"x"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"unknown-component","schema":"test","input":"root = Card([Mystery(\"a\"), Text(\"b\")])","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"b"},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"unknown-component","component":"Mystery","path":"","message":"Unknown component \"Mystery\" — not found in catalog or builtins. Available components: Card, Text, Button, Input, Chart, Tagged","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"excess-args","schema":"test","input":"root = Card([Text(\"a\", \"m\", \"extra\", 4)])","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"a","size":"m"},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"excess-args","component":"Text","path":"","message":"Text takes 2 arg(s), got 4 (2 excess dropped)","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"type-mismatch-scalar","schema":"test","input":"root = Card([Text(42)], 7)","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"type-mismatch","component":"Text","path":"/text","message":"field \"/text\" expects string but got number","statementId":"root"},{"code":"type-mismatch","component":"Card","path":"/title","message":"field \"/title\" expects string but got number","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"enum-mismatch","schema":"test","input":"root = Card([Text(\"a\", \"xl\"), Button(\"b\", null, \"tertiary\")])","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"a","size":"m"},"partial":false,"hasDynamicProps":false},{"type":"element","typeName":"Button","props":{"label":"b","action":null},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"type-mismatch","component":"Text","path":"/size","message":"field \"/size\" expects one of [\"s\", \"m\", \"l\"] but got \"xl\"","statementId":"root"},{"code":"type-mismatch","component":"Button","path":"/variant","message":"field \"/variant\" expects one of [\"primary\", \"secondary\"] but got \"tertiary\"","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"enum-partial-deferred","schema":"test","input":"root = Card([Text(\"a\", \"x","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"a","size":"x"},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"integer-boolean-const","schema":"test","input":"root = Card([x])\nx = Tagged(1.5, \"yes\", \"y\", \"loud\")","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Tagged","props":{"count":1.5,"tone":"loud"},"partial":false,"hasDynamicProps":false,"statementId":"x"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[{"code":"type-mismatch","component":"Tagged","path":"/flag","message":"field \"/flag\" expects boolean but got string","statementId":"x"},{"code":"type-mismatch","component":"Tagged","path":"/kind","message":"field \"/kind\" expects one of [\"x\"] but got \"y\"","statementId":"x"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"nested-object","schema":"test","input":"root = Card([c1, c2, c3])\nc1 = Chart([\"a\"], [1], {unit: \"kg\"})\nc2 = Chart([\"a\"], [1], {scale: 2})\nc3 = Chart([\"a\"], [1], {unit: 5, scale: \"big\"})","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Chart","props":{"labels":["a"],"values":[1],"meta":{"unit":"kg"}},"partial":false,"hasDynamicProps":false,"statementId":"c1"},{"type":"element","typeName":"Chart","props":{"labels":["a"],"values":[1]},"partial":false,"hasDynamicProps":false,"statementId":"c2"},{"type":"element","typeName":"Chart","props":{"labels":["a"],"values":[1]},"partial":false,"hasDynamicProps":false,"statementId":"c3"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":4,"errors":[{"code":"missing-required","component":"Chart","path":"/meta/unit","message":"missing required field \"/meta/unit\"","statementId":"c2"},{"code":"type-mismatch","component":"Chart","path":"/meta/unit","message":"field \"/meta/unit\" expects string but got number","statementId":"c3"},{"code":"type-mismatch","component":"Chart","path":"/meta/scale","message":"field \"/meta/scale\" expects number but got string","statementId":"c3"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"array-items-pruned","schema":"test","input":"root = Card([Chart([\"a\", 2, \"c\"], [1, \"x\", 3])])","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Chart","props":{"labels":["a","c"],"values":[1,3]},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"type-mismatch","component":"Chart","path":"/labels/1","message":"field \"/labels/1\" expects string but got number","statementId":"root"},{"code":"type-mismatch","component":"Chart","path":"/values/1","message":"field \"/values/1\" expects number but got string","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"element-in-data-slot","schema":"test","input":"root = Card([Chart([Text(\"a\")], [1])])","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Chart","props":{"labels":[],"values":[1]},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"type-mismatch","component":"Chart","path":"/labels/0","message":"field \"/labels/0\" expects string but got component \"Text\"","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"unresolved-and-orphaned","schema":"test","input":"root = Card([a, missing])\na = Text(\"x\")\nlonely = Text(\"y\")","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"x"},"partial":false,"hasDynamicProps":false,"statementId":"a"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["missing"],"orphaned":["lonely"],"statementCount":3,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"cycle","schema":"test","input":"root = Card([a])\na = Card([b])\nb = Card([a])","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"b"}]},"partial":false,"hasDynamicProps":false,"statementId":"a"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["a"],"orphaned":[],"statementCount":3,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"self-reference","schema":"test","input":"root = Card([root])","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["root"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"no-root-first-component","schema":"test","input":"x = 1\nmain = Card([Text(\"a\")])\nother = Card([])","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"a"},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"main"},"meta":{"incomplete":false,"unresolved":[],"orphaned":["x","other"],"statementCount":3,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"root-name-statement","schema":"test","input":"Card = Card([Text(\"named\")])\nother = Card([])","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"named"},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"Card"},"meta":{"incomplete":false,"unresolved":[],"orphaned":["other"],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"duplicate-ids","schema":"test","input":"root = Card([a])\na = Text(\"first\")\na = Text(\"second\")","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"second"},"partial":false,"hasDynamicProps":false,"statementId":"a"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"only-prose","schema":"test","input":"Here is your UI, it will appear below.","expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"empty","schema":"test","input":"","expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"whitespace","schema":"test","input":" \n\t \n","expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"fences-and-prose","schema":"test","input":"Sure! Here it is:\n```openui\nroot = Card([a])\na = Text(\"fenced\")\n```\nAnything else?","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"fenced"},"partial":false,"hasDynamicProps":false,"statementId":"a"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"multiple-fences","schema":"test","input":"```\nroot = Card([a])\n```\nand\n```\na = Text(\"second block\")\n```","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"second block"},"partial":false,"hasDynamicProps":false,"statementId":"a"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"unclosed-fence","schema":"test","input":"```openui\nroot = Card([a])\na = Text(\"str","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"str"},"partial":true,"hasDynamicProps":false,"statementId":"a"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"fence-in-string","schema":"test","input":"root = Card([a])\na = Text(\"use ``` to fence\")","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"use ``` to fence"},"partial":false,"hasDynamicProps":false,"statementId":"a"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"comments","schema":"test","input":"root = Card([a]) // trailing\n# full line\na = Text(\"not // a comment # here\")\n// done","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"not // a comment # here"},"partial":false,"hasDynamicProps":false,"statementId":"a"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"crlf","schema":"test","input":"root = Card([a])\r\na = Text(\"windows\")\r\n","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"windows"},"partial":false,"hasDynamicProps":false,"statementId":"a"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"string-escapes","schema":"test","input":"root = Card([a, b, c, d])\na = Text(\"tab\\tnew\\nquote\\\" back\\\\ uni\\u00e9 pair\\ud83d\\ude00\")\nb = Text('single \\' quote \\n esc \\q')\nc = Text(\"bad \\x escape\")\nd = Text(\"emoji 😀 and é and 漢字\")","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"tab\tnew\nquote\" back\\ unié pair😀"},"partial":false,"hasDynamicProps":false,"statementId":"a"},{"type":"element","typeName":"Text","props":{"text":"single ' quote \n esc q"},"partial":false,"hasDynamicProps":false,"statementId":"b"},{"type":"element","typeName":"Text","props":{"text":"bad \\x escape"},"partial":false,"hasDynamicProps":false,"statementId":"c"},{"type":"element","typeName":"Text","props":{"text":"emoji 😀 and é and 漢字"},"partial":false,"hasDynamicProps":false,"statementId":"d"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":5,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"emoji-outside-strings","schema":"test","input":"root = Card([a]) 😀\na = Text(\"ok\") ✅","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"ok"},"partial":false,"hasDynamicProps":false,"statementId":"a"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"unterminated-string","schema":"test","input":"root = Card([a])\na = Text(\"streaming tex","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"streaming tex"},"partial":true,"hasDynamicProps":false,"statementId":"a"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"unterminated-escape","schema":"test","input":"root = Card([a])\na = Text(\"ends with \\","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"ends with \\"},"partial":true,"hasDynamicProps":false,"statementId":"a"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"numbers","schema":"test","input":"root = Card([Chart([\"a\",\"b\",\"c\",\"d\",\"e\",\"f\"], [-3, 1.5, 1e3, 007, -0, 2.5e-3])])\n$x = 1e\n$y = 1E+2\n$z = 3.","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Chart","props":{"labels":["a","b","c","d","e","f"],"values":[-3,1.5,1000,7,0,0.0025]},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":4,"errors":[]},"stateDeclarations":{"$x":null,"$y":100,"$z":{"k":"Member","obj":{"k":"Num","v":3},"field":"?"}},"queryStatements":[],"mutationStatements":[]}}, +{"name":"operators","schema":"test","input":"$a = 1\n$b = \"2\"\nroot = Card([Text($a + $b * 2 - -1 > 3 && !$c || $a == \"1\" ? \"yes\" : \"no\")])","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":{"k":"Ternary","cond":{"k":"BinOp","op":"||","left":{"k":"BinOp","op":"&&","left":{"k":"BinOp","op":">","left":{"k":"BinOp","op":"-","left":{"k":"BinOp","op":"+","left":{"k":"StateRef","n":"$a"},"right":{"k":"BinOp","op":"*","left":{"k":"StateRef","n":"$b"},"right":{"k":"Num","v":2}}},"right":{"k":"Num","v":-1}},"right":{"k":"Num","v":3}},"right":{"k":"UnaryOp","op":"!","operand":{"k":"StateRef","n":"$c"}}},"right":{"k":"BinOp","op":"==","left":{"k":"StateRef","n":"$a"},"right":{"k":"Str","v":"1"}}},"then":{"k":"Str","v":"yes"},"else":{"k":"Str","v":"no"}}},"partial":false,"hasDynamicProps":true}]},"partial":false,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":3,"errors":[]},"stateDeclarations":{"$a":1,"$b":"2","$c":null},"queryStatements":[],"mutationStatements":[]}}, +{"name":"multiline-ternary","schema":"test","input":"$on = true\nroot = Card([t])\nt = Text($on\n ? \"enabled\"\n : \"disabled\")","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":{"k":"Ternary","cond":{"k":"StateRef","n":"$on"},"then":{"k":"Str","v":"enabled"},"else":{"k":"Str","v":"disabled"}}},"partial":false,"hasDynamicProps":true,"statementId":"t"}]},"partial":false,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":3,"errors":[]},"stateDeclarations":{"$on":true},"queryStatements":[],"mutationStatements":[]}}, +{"name":"member-index","schema":"test","input":"$data = {rows: [{name: \"a\"}, {name: \"b\"}], \"key x\": 1}\nroot = Card([Text($data.rows[1].name), Text($data.rows.name), Text($data[\"key x\"])])","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":{"k":"Member","obj":{"k":"Index","obj":{"k":"Member","obj":{"k":"StateRef","n":"$data"},"field":"rows"},"index":{"k":"Num","v":1}},"field":"name"}},"partial":false,"hasDynamicProps":true},{"type":"element","typeName":"Text","props":{"text":{"k":"Member","obj":{"k":"Member","obj":{"k":"StateRef","n":"$data"},"field":"rows"},"field":"name"}},"partial":false,"hasDynamicProps":true},{"type":"element","typeName":"Text","props":{"text":{"k":"Index","obj":{"k":"StateRef","n":"$data"},"index":{"k":"Str","v":"key x"}}},"partial":false,"hasDynamicProps":true}]},"partial":false,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{"$data":{"rows":[{"name":"a"},{"name":"b"}],"key x":1}},"queryStatements":[],"mutationStatements":[]}}, +{"name":"builtins","schema":"test","input":"$items = [3, 1, 2]\nroot = Card([Text(\"\" + @Count($items)), Text(\"\" + @Sum($items)), Text(@Round(2.555, 2) + \"\")])\nbare = Count($items)","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":{"k":"BinOp","op":"+","left":{"k":"Str","v":""},"right":{"k":"Comp","name":"Count","args":[{"k":"StateRef","n":"$items"}]}}},"partial":false,"hasDynamicProps":true},{"type":"element","typeName":"Text","props":{"text":{"k":"BinOp","op":"+","left":{"k":"Str","v":""},"right":{"k":"Comp","name":"Sum","args":[{"k":"StateRef","n":"$items"}]}}},"partial":false,"hasDynamicProps":true},{"type":"element","typeName":"Text","props":{"text":{"k":"BinOp","op":"+","left":{"k":"Comp","name":"Round","args":[{"k":"Num","v":2.555},{"k":"Num","v":2}]},"right":{"k":"Str","v":""}}},"partial":false,"hasDynamicProps":true}]},"partial":false,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":["bare"],"statementCount":3,"errors":[]},"stateDeclarations":{"$items":[3,1,2]},"queryStatements":[],"mutationStatements":[]}}, +{"name":"actions","schema":"test","input":"$n = 0\nroot = Card([b1, b2, b3])\nb1 = Button(\"Inc\", Action([@Set($n, $n + 1), @ToAssistant(\"Bumped\", \"ctx\")]))\nb2 = Button(\"Reset\", Action([@Reset($n)]))\nb3 = Button(\"Docs\", Action([@OpenUrl(\"https://openui.com\")]))","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Button","props":{"label":"Inc","action":{"k":"Comp","name":"Action","args":[{"k":"Arr","els":[{"k":"Comp","name":"Set","args":[{"k":"StateRef","n":"$n"},{"k":"BinOp","op":"+","left":{"k":"StateRef","n":"$n"},"right":{"k":"Num","v":1}}]},{"k":"Comp","name":"ToAssistant","args":[{"k":"Str","v":"Bumped"},{"k":"Str","v":"ctx"}]}]}]}},"partial":false,"hasDynamicProps":true,"statementId":"b1"},{"type":"element","typeName":"Button","props":{"label":"Reset","action":{"k":"Comp","name":"Action","args":[{"k":"Arr","els":[{"k":"Comp","name":"Reset","args":[{"k":"StateRef","n":"$n"}]}]}]}},"partial":false,"hasDynamicProps":true,"statementId":"b2"},{"type":"element","typeName":"Button","props":{"label":"Docs","action":{"k":"Comp","name":"Action","args":[{"k":"Arr","els":[{"k":"Comp","name":"OpenUrl","args":[{"k":"Str","v":"https://openui.com"}]}]}]}},"partial":false,"hasDynamicProps":true,"statementId":"b3"}]},"partial":false,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":5,"errors":[]},"stateDeclarations":{"$n":0},"queryStatements":[],"mutationStatements":[]}}, +{"name":"each","schema":"test","input":"$todos = [{title: \"a\", done: false}, {title: \"b\", done: true}]\nroot = Card(@Each($todos, todo, Text(todo.title)))","expected":{"root":{"type":"element","typeName":"Card","props":{"children":{"k":"Comp","name":"Each","args":[{"k":"StateRef","n":"$todos"},{"k":"Ref","n":"todo"},{"k":"Comp","name":"Text","args":[{"k":"Member","obj":{"k":"Ref","n":"todo"},"field":"title"}],"mappedProps":{"text":{"k":"Member","obj":{"k":"Ref","n":"todo"},"field":"title"}}}]}},"partial":false,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{"$todos":[{"title":"a","done":false},{"title":"b","done":true}]},"queryStatements":[],"mutationStatements":[]}}, +{"name":"query-mutation","schema":"test","input":"$q = \"x\"\nusers = Query(\"list_users\", {search: $q, page: $page}, {rows: []}, 30)\nsave = Mutation(\"save_user\", {name: $q})\nroot = Card([Text(users.rows.length + \" users\"), Button(\"Save\", Action([@Run(save), @Run(users)]))])","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":{"k":"BinOp","op":"+","left":{"k":"Member","obj":{"k":"Member","obj":{"k":"RuntimeRef","n":"users","refType":"query"},"field":"rows"},"field":"length"},"right":{"k":"Str","v":" users"}}},"partial":false,"hasDynamicProps":true},{"type":"element","typeName":"Button","props":{"label":"Save","action":{"k":"Comp","name":"Action","args":[{"k":"Arr","els":[{"k":"Comp","name":"Run","args":[{"k":"RuntimeRef","n":"save","refType":"mutation"}]},{"k":"Comp","name":"Run","args":[{"k":"RuntimeRef","n":"users","refType":"query"}]}]}]}},"partial":false,"hasDynamicProps":true}]},"partial":false,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":4,"errors":[]},"stateDeclarations":{"$q":"x","$page":null},"queryStatements":[{"statementId":"users","toolAST":{"k":"Str","v":"list_users"},"argsAST":{"k":"Obj","entries":[["search",{"k":"StateRef","n":"$q"}],["page",{"k":"StateRef","n":"$page"}]]},"defaultsAST":{"k":"Obj","entries":[["rows",{"k":"Arr","els":[]}]]},"refreshAST":{"k":"Num","v":30},"deps":["$q","$page"],"complete":true}],"mutationStatements":[{"statementId":"save","toolAST":{"k":"Str","v":"save_user"},"argsAST":{"k":"Obj","entries":[["name",{"k":"StateRef","n":"$q"}]]}}]}}, +{"name":"state-query","schema":"test","input":"$data = Query(\"get\", {})\nroot = Card([Text(\"x\")])","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"x"},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[{"statementId":"$data","toolAST":{"k":"Str","v":"get"},"argsAST":{"k":"Obj","entries":[]},"defaultsAST":null,"refreshAST":null,"complete":true}],"mutationStatements":[]}}, +{"name":"inline-query","schema":"test","input":"root = Card([Text(Query(\"get\", {}))])","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"inline-reserved","component":"Query","path":"","message":"Query() must be declared as a top-level statement, not used inline as a value","statementId":"root"},{"code":"null-required","component":"Text","path":"/text","message":"required field \"/text\" cannot be null — signature: Text(text*: string, size: \"s\"|\"m\"|\"l\")","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"assignment","schema":"test","input":"$v = \"\"\nroot = Card([Input(\"name\", $v = $value)])","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Input","props":{"name":"name","value":{"k":"Assign","target":"$v","value":{"k":"StateRef","n":"$value"}}},"partial":false,"hasDynamicProps":true}]},"partial":false,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{"$v":"","$value":null},"queryStatements":[],"mutationStatements":[]}}, +{"name":"object-key-order","schema":"test","input":"$o = {b: 1, 2: \"two\", a: 3, 1: \"one\", \"10\": 10}\nroot = Card([Text(\"x\")])","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"x"},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{"$o":{"1":"one","2":"two","10":10,"b":1,"a":3}},"queryStatements":[],"mutationStatements":[]}}, +{"name":"missing-commas","schema":"test","input":"root = Card([Text(\"a\") Text(\"b\"), Text(\"c\"),])","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"a"},"partial":false,"hasDynamicProps":false},{"type":"element","typeName":"Text","props":{"text":"b"},"partial":false,"hasDynamicProps":false},{"type":"element","typeName":"Text","props":{"text":"c"},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"lowercase-builtin-name","schema":"test","input":"root = Card([Text(@first([1]))])","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"unknown-component","component":"first","path":"","message":"Unknown component \"first\" — not found in catalog or builtins. Available components: Card, Text, Button, Input, Chart, Tagged","statementId":"root"},{"code":"null-required","component":"Text","path":"/text","message":"required field \"/text\" cannot be null — signature: Text(text*: string, size: \"s\"|\"m\"|\"l\")","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"unknown-tokens","schema":"test","input":"root = Card([Text(\"a\")]) ; ~ ^\n$ = 3\n= 4\nfoo","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"a"},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"combining-characters","schema":"test","input":"root = Card([Text(\"e\\u0301 cafe\\u0301\"), Text(\"한국어\"), Text(\"ok\")])","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"é café"},"partial":false,"hasDynamicProps":false},{"type":"element","typeName":"Text","props":{"text":"한국어"},"partial":false,"hasDynamicProps":false},{"type":"element","typeName":"Text","props":{"text":"ok"},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"truncated-mid-call","schema":"test","input":"root = Card([a, b])\na = Text(\"one\")\nb = Button(\"tw","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"one"},"partial":true,"hasDynamicProps":false,"statementId":"a"},{"type":"element","typeName":"Button","props":{"label":"tw"},"partial":true,"hasDynamicProps":false,"statementId":"b"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":3,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"truncated-mid-object","schema":"test","input":"root = Card([Chart([\"a\"], [1], {unit: \"k","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Chart","props":{"labels":["a"],"values":[1],"meta":{"unit":"k"}},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"data-in-component-slot","schema":"test","input":"root = Card([Text(\"hi\"), { text: \"a\" }, \"b\", 3, [Text(\"x\")], true])","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"hi"},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"type-mismatch","component":"Card","path":"/children/1","message":"field \"/children/1\" expects Text | Button | Chart but got plain object","statementId":"root"},{"code":"type-mismatch","component":"Card","path":"/children/2","message":"field \"/children/2\" expects Text | Button | Chart but got string","statementId":"root"},{"code":"type-mismatch","component":"Card","path":"/children/3","message":"field \"/children/3\" expects Text | Button | Chart but got number","statementId":"root"},{"code":"type-mismatch","component":"Card","path":"/children/4","message":"field \"/children/4\" expects Text | Button | Chart but got array","statementId":"root"},{"code":"type-mismatch","component":"Card","path":"/children/5","message":"field \"/children/5\" expects Text | Button | Chart but got boolean","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"data-in-component-slot-via-ref","schema":"test","input":"root = Card(items)\nitems = [{ text: \"a\" }]","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[{"code":"type-mismatch","component":"Card","path":"/children/0","message":"field \"/children/0\" expects Text | Button | Chart but got plain object","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"hoisted-data-def","schema":"hoisted","input":"root = Tree({ label: \"a\" })","expected":{"root":{"type":"element","typeName":"Tree","props":{"node":{"label":"a"}},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}, +{"name":"cloud-envelope","schema":"chat","input":"]]>openui:content?thesys=true\n```openui-lang\nroot = Card([h, t])\nh = CardHeader(\"Q3\", \"Revenue\")\nt = TextContent(\"Up 12%\")\n```\n]]>openui:end","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Q3","subtitle":"Revenue"},"partial":false,"hasDynamicProps":false,"statementId":"h"},{"type":"element","typeName":"TextContent","props":{"text":"Up 12%"},"partial":false,"hasDynamicProps":false,"statementId":"t"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":3,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}} +] diff --git a/packages/swift-lang/Tests/OpenUILangTests/Fixtures/prompts.json b/packages/swift-lang/Tests/OpenUILangTests/Fixtures/prompts.json new file mode 100644 index 0000000000..000c4bcf60 --- /dev/null +++ b/packages/swift-lang/Tests/OpenUILangTests/Fixtures/prompts.json @@ -0,0 +1,13 @@ +[ +{"name":"chat-default","spec":{"root":"Card","components":{"Card":{"signature":"Card(children: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock | ListBlock | FollowUpBlock | SectionBlock | Tabs | Carousel)[], sources?: {url?: string, title: string, sourceName: string}[])","description":"Vertical container for all content in a chat response. Children stack top to bottom automatically. Optional sources ([{ title, sourceName, url }]) render as a Sources strip at the bottom and back inline [n] citations in TextContent."},"CardHeader":{"signature":"CardHeader(title?: string, subtitle?: string)","description":"Header with optional title and subtitle"},"TextContent":{"signature":"TextContent(text: string, size?: \"small\" | \"default\" | \"large\" | \"small-heavy\" | \"large-heavy\")","description":"Text block. Supports markdown. Optional size: \"small\" | \"default\" | \"large\" | \"small-heavy\" | \"large-heavy\"."},"MarkDownRenderer":{"signature":"MarkDownRenderer(textMarkdown: string, variant?: \"clear\" | \"card\" | \"sunk\")","description":"Renders markdown text with optional container variant"},"Callout":{"signature":"Callout(variant: \"info\" | \"warning\" | \"error\" | \"success\" | \"neutral\", title: string, description: string, visible?: $binding)","description":"Callout banner. Optional visible is a reactive $boolean — auto-dismisses after 3s by setting $visible to false."},"TextCallout":{"signature":"TextCallout(variant?: \"neutral\" | \"info\" | \"warning\" | \"success\" | \"danger\", title?: string, description?: string)","description":"Text callout with variant, title, and description"},"Image":{"signature":"Image(alt: string, src?: string)","description":"Image with alt text and optional URL"},"ImageBlock":{"signature":"ImageBlock(src: string, alt?: string)","description":"Image block with loading state"},"ImageGallery":{"signature":"ImageGallery(images: {src: string, alt?: string, details?: string}[])","description":"Gallery grid of images with modal preview"},"CodeBlock":{"signature":"CodeBlock(language: string, codeString: string)","description":"Syntax-highlighted code block"},"Separator":{"signature":"Separator(orientation?: \"horizontal\" | \"vertical\", decorative?: boolean)","description":"Visual divider between content sections"},"Table":{"signature":"Table(columns: Col[])","description":"Data table — column-oriented. Each Col holds its own data array."},"Col":{"signature":"Col(label: string, data: any, type?: \"string\" | \"number\" | \"action\")","description":"Column definition — holds label + data array"},"BarChart":{"signature":"BarChart(labels: string[], series: Series[], variant?: \"grouped\" | \"stacked\", xLabel?: string, yLabel?: string, height?: number)","description":"Vertical bars; use for comparing values across categories with one or more series"},"LineChart":{"signature":"LineChart(labels: string[], series: Series[], variant?: \"linear\" | \"natural\" | \"step\", xLabel?: string, yLabel?: string, height?: number)","description":"Lines over categories; use for trends and continuous data over time"},"AreaChart":{"signature":"AreaChart(labels: string[], series: Series[], variant?: \"linear\" | \"natural\" | \"step\", xLabel?: string, yLabel?: string, height?: number)","description":"Filled area under lines; use for cumulative totals or volume trends over time"},"RadarChart":{"signature":"RadarChart(labels: string[], series: Series[])","description":"Spider/web chart; use for comparing multiple variables across one or more entities"},"HorizontalBarChart":{"signature":"HorizontalBarChart(labels: string[], series: Series[], variant?: \"grouped\" | \"stacked\", xLabel?: string, yLabel?: string)","description":"Horizontal bars; prefer when category labels are long or for ranked lists"},"Series":{"signature":"Series(category: string, values: number[])","description":"One data series"},"PieChart":{"signature":"PieChart(labels: string[], values: number[], variant?: \"pie\" | \"donut\", appearance?: \"circular\" | \"semiCircular\")","description":"Circular slices; use plucked arrays: PieChart(data.categories, data.values)"},"RadialChart":{"signature":"RadialChart(labels: string[], values: number[])","description":"Radial bars; use plucked arrays: RadialChart(data.categories, data.values)"},"SingleStackedBarChart":{"signature":"SingleStackedBarChart(labels: string[], values: number[])","description":"Single horizontal stacked bar; use plucked arrays: SingleStackedBarChart(data.categories, data.values)"},"Slice":{"signature":"Slice(category: string, value: number)","description":"One slice with label and numeric value"},"ScatterChart":{"signature":"ScatterChart(datasets: ScatterSeries[], xLabel?: string, yLabel?: string)","description":"X/Y scatter plot; use for correlations, distributions, and clustering"},"ScatterSeries":{"signature":"ScatterSeries(name: string, points: Point[])","description":"Named dataset"},"Point":{"signature":"Point(x: number, y: number, z?: number)","description":"Data point with numeric coordinates"},"Form":{"signature":"Form(name: string, buttons: Buttons, fields?: FormControl[])","description":"Form container with fields and explicit action buttons"},"FormControl":{"signature":"FormControl(label: string, input: Input | TextArea | Select | DatePicker | Slider | CheckBoxGroup | RadioGroup | Chips | OptionCards, hint?: string)","description":"Field with label, input component, and optional hint text"},"Label":{"signature":"Label(text: string)","description":"Text label"},"Input":{"signature":"Input(name: string, placeholder?: string, type?: \"text\" | \"email\" | \"password\" | \"number\" | \"url\", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)","description":""},"TextArea":{"signature":"TextArea(name: string, placeholder?: string, rows?: number, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)","description":""},"Select":{"signature":"Select(name: string, items: SelectItem[], placeholder?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding, size?: \"small\" | \"medium\" | \"large\")","description":""},"SelectItem":{"signature":"SelectItem(value: string, label: string)","description":"Option for Select"},"DatePicker":{"signature":"DatePicker(name: string, mode?: \"single\" | \"range\", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)","description":""},"Slider":{"signature":"Slider(name: string, variant: \"continuous\" | \"discrete\", min: number, max: number, step?: number, defaultValue?: number[], label?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)","description":"Numeric slider input; supports continuous and discrete (stepped) variants"},"CheckBoxGroup":{"signature":"CheckBoxGroup(name: string, items: CheckBoxItem[], rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding>)","description":""},"CheckBoxItem":{"signature":"CheckBoxItem(label: string, description: string, name: string, defaultChecked?: boolean)","description":""},"RadioGroup":{"signature":"RadioGroup(name: string, items: RadioItem[], defaultValue?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)","description":""},"RadioItem":{"signature":"RadioItem(label: string, description: string, value: string)","description":""},"SwitchGroup":{"signature":"SwitchGroup(name: string, items: SwitchItem[], variant?: \"clear\" | \"card\" | \"sunk\", value?: $binding>)","description":"Group of switch toggles"},"SwitchItem":{"signature":"SwitchItem(label?: string, description?: string, name: string, defaultChecked?: boolean)","description":"Individual switch toggle"},"Button":{"signature":"Button(label: string, action?: ActionExpression, variant?: \"primary\" | \"secondary\" | \"tertiary\", type?: \"normal\" | \"destructive\", size?: \"extra-small\" | \"small\" | \"medium\" | \"large\")","description":"Clickable button"},"Buttons":{"signature":"Buttons(buttons: Button[], direction?: \"row\" | \"column\")","description":"Group of Button components. direction: \"row\" (default) | \"column\"."},"ListBlock":{"signature":"ListBlock(items: ListItem[], variant?: \"number\" | \"image\", size?: \"default\" | \"small\")","description":"A list of items with number or image indicators. Each item can optionally have an action. size small renders a compact list."},"ListItem":{"signature":"ListItem(title: string, subtitle?: string, image?: {src: string, alt: string}, actionLabel?: string, action?: ActionExpression)","description":"Item in a ListBlock — displays a title with an optional subtitle and image. When action is provided, the item becomes clickable."},"FollowUpBlock":{"signature":"FollowUpBlock(items: FollowUpItem[])","description":"List of clickable follow-up suggestions placed at the end of a response"},"FollowUpItem":{"signature":"FollowUpItem(text: string)","description":"Clickable follow-up suggestion — when clicked, sends text as user message"},"SectionBlock":{"signature":"SectionBlock(sections: SectionItem[], isFoldable?: boolean)","description":"Collapsible accordion sections. Auto-opens sections as they stream in. Use SectionItem for each section."},"SectionItem":{"signature":"SectionItem(value: string, trigger: string, content: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock | ListBlock | FollowUpBlock | Tabs | Accordion)[])","description":"Section with a label and collapsible content — used inside SectionBlock"},"Tabs":{"signature":"Tabs(items: TabItem[])","description":"Tabbed container"},"TabItem":{"signature":"TabItem(value: string, trigger: string, content: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock | ListBlock | FollowUpBlock | Accordion)[])","description":"value is unique id, trigger is tab label, content is array of components"},"Accordion":{"signature":"Accordion(items: AccordionItem[])","description":"Collapsible sections"},"AccordionItem":{"signature":"AccordionItem(value: string, trigger: string, content: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock | ListBlock | FollowUpBlock)[])","description":"value is unique id, trigger is section title"},"Steps":{"signature":"Steps(items: StepsItem[])","description":"Step-by-step guide"},"StepsItem":{"signature":"StepsItem(title: string, details: string)","description":"title and details text for one step"},"Carousel":{"signature":"Carousel(children: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock | ListBlock | FollowUpBlock)[][], variant?: \"card\" | \"sunk\")","description":"Horizontal scrollable carousel"},"TagBlock":{"signature":"TagBlock(tags: string[], size?: \"sm\" | \"md\" | \"lg\")","description":"tags is an array of strings; optional size sm | md | lg"},"Tag":{"signature":"Tag(text: string, icon?: Icon, size?: \"sm\" | \"md\" | \"lg\", variant?: \"neutral\" | \"info\" | \"success\" | \"warning\" | \"danger\")","description":"Styled tag/badge with optional Icon and variant"},"EntityList":{"signature":"EntityList(rows?: {left: string, right: string, rightVariant?: \"text\" | \"number\"}[], size?: \"small\" | \"default\", header?: {left: string, right: string, rightVariant?: \"text\" | \"number\"}, footer?: {left: string, right: string, rightVariant?: \"text\" | \"number\"})","description":"Two-column key/value rows (left label, right value). size 'default' supports optional header and footer rows; rightVariant 'number' uses tabular numbers."},"InlineHeader":{"signature":"InlineHeader(heading: string, description?: string)","description":"Compact section heading with an optional one-line description, for use inside cards."},"Icon":{"signature":"Icon(name: string, category?: string)","description":"A lucide icon by kebab-case name (e.g. 'circle-check'). Optional category picks a topical fallback when the name doesn't resolve."},"IconButton":{"signature":"IconButton(name: string, icon: Icon, action?: ActionExpression, variant?: \"primary\" | \"secondary\" | \"tertiary\", size?: \"extra-small\" | \"small\" | \"medium\" | \"large\", shape?: \"square\" | \"circle\")","description":"Icon-only button. name is the accessible label and the action label; icon is an Icon; action fires on click."},"EditableTable":{"signature":"EditableTable(name?: string, columns?: {type: \"text\" | \"number\" | \"date-single\" | \"select\" | \"url\", key?: string, header?: string, width?: number, options?: {value: string, label: string}[]}[], data?: {id: string, values: (string | number)[]}[])","description":"Spreadsheet-like table whose cells the user can edit inline (text, number, url, date, select columns); edits are saved back as a form field"},"ChipItem":{"signature":"ChipItem(value: string, label: string, icon?: Icon, disabled?: boolean)","description":"A single selectable chip inside a Chips group, with a value, label and optional icon."},"Chips":{"signature":"Chips(name: string, type?: \"single\" | \"multiple\", items?: ChipItem[], rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, defaultValue?: string | string[])","description":"A form field of compact selectable chips for choosing one or many short options; the selection is stored under `name`."},"OptionCard":{"signature":"OptionCard(value: string, title: string, subtitle?: string, topContent?: Icon | Image, disabled?: boolean)","description":"A single selectable card inside an OptionCards group, with a value, title, optional subtitle and an optional Icon or Image on top."},"OptionCards":{"signature":"OptionCards(name: string, type?: \"single\" | \"multiple\", items?: OptionCard[], rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, defaultValue?: string | string[])","description":"A form field of selectable cards (title, optional subtitle, optional icon or image) laid out in a responsive grid for choosing one or many options; the selection is stored under `name`."},"Text":{"signature":"Text(variant?: \"text\" | \"number\", value: string, subtext?: string, subtextVariant?: \"text\" | \"number\" | \"metric\", size?: \"xs\" | \"sm\" | \"md\" | \"lg\")","description":"Plain text line with optional subtext. variant 'number' uses tabular number styling; subtextVariant 'metric' colors a leading +/- subtext green/red."},"BoldText":{"signature":"BoldText(variant?: \"text\" | \"number\", value: string, subtext?: string, subtextVariant?: \"text\" | \"number\" | \"metric\", size?: \"xs\" | \"sm\" | \"md\" | \"lg\")","description":"Emphasized (bold) text line with optional subtext. variant 'number' uses tabular number styling; subtextVariant 'metric' colors a leading +/- subtext green/red."},"IconText":{"signature":"IconText(icon: Icon, iconVariant?: \"neutral\" | \"info\" | \"success\" | \"warning\" | \"danger\" | \"inverted\" | \"filled\" | \"soft\", iconSize?: \"xs\" | \"s\" | \"m\" | \"l\" | \"xl\" | \"sm\" | \"md\" | \"lg\", title: string, subtitle?: string, bold?: boolean, layout?: \"horizontal\" | \"vertical\")","description":"An icon badge with a title and optional subtitle, laid out horizontally or vertically. iconVariant sets the badge color."},"ImageText":{"signature":"ImageText(src: string, alt?: string, title: string, subtitle?: string, bold?: boolean, layout?: \"horizontal\" | \"vertical\", imageSize?: number)","description":"A small square image (thumbnail/avatar) with a title and optional subtitle. src must be a real image URL."},"ImageTextLarge":{"signature":"ImageTextLarge(src: string, alt?: string, title: string, subtitle?: string, bold?: boolean)","description":"A full-width banner image above a bold title and optional subtitle. src must be a real image URL."},"MetricIndicatorInline":{"signature":"MetricIndicatorInline(value: string, subtext?: string, trend?: {direction: \"up\" | \"down\", value: number})","description":"Headline metric value with an optional +/- percentage trend and subtext, all on one line."},"MetricIndicatorWithStrikethrough":{"signature":"MetricIndicatorWithStrikethrough(value: string, subtext?: string, previousValue?: string, trend?: {direction: \"up\" | \"down\", value: number})","description":"Headline metric value with an optional struck-through previousValue, a +/- percentage trend, and subtext below."},"SnippetCardItem":{"signature":"SnippetCardItem(id?: string, lhs: IconText | ImageText, rhs?: Text | BoldText)","description":"One row-style snippet card: a label on the left (IconText or ImageText) and an optional value on the right (Text or BoldText)."},"SnippetCardBlock":{"signature":"SnippetCardBlock(items: SnippetCardItem[], layout?: \"grid\", responsive?: boolean, action?: ActionExpression, gap?: number | string)","description":"A responsive grid of compact label/value cards (2 per row) for showing several short facts side by side; optionally clickable with a shared action."},"OverviewCardItem":{"signature":"OverviewCardItem(id?: string, top: IconText | ImageText | Text, bottom?: MetricIndicatorInline)","description":"One overview card: a heading slot at the top (IconText, ImageText or Text) and an optional MetricIndicatorInline at the bottom."},"OverviewCardBlock":{"signature":"OverviewCardBlock(items: OverviewCardItem[], layout?: \"grid\" | \"carousel\", responsive?: boolean, action?: ActionExpression, gap?: number | string)","description":"A grid or horizontal carousel of compact overview cards, each with a heading (icon/image/text) on top and an inline metric below; optionally clickable with a shared action."},"ContextCardItem":{"signature":"ContextCardItem(id?: string, title: string | Tag, body?: string, bgColor?: \"gray\", bgImageSrc?: string, bgImageAlt?: string)","description":"A single card inside a ContextCardBlock: a title (plain string or Tag), an optional markdown body, and an optional gray tint or background image."},"ContextCardBlock":{"signature":"ContextCardBlock(items: ContextCardItem[], layout?: \"grid\" | \"carousel\", responsive?: boolean, action?: ActionExpression, gap?: number | string)","description":"A grid or carousel of compact tinted context cards (title or tag plus a short bold body); an optional action makes every card clickable."},"CompositeCardItem":{"signature":"CompositeCardItem(id?: string, header?: IconText | ImageText | ImageTextLarge | Text | Image, body?: (Text | BoldText | MetricIndicatorInline | IconText | Image | AreaChart | BarChart | LineChart | ListBlock | TagBlock | EntityList)[], footer?: {price?: BoldText | MetricIndicatorWithStrikethrough, button?: Button})","description":"A single card inside a CompositeCardBlock: an optional header (icon/image/text), a stack of body elements (text, metrics, charts, lists, tags), and an optional price/button footer."},"CompositeCardBlock":{"signature":"CompositeCardBlock(items: CompositeCardItem[], layout?: \"grid\" | \"carousel\", responsive?: boolean, action?: ActionExpression, gap?: number | string)","description":"A two-per-row grid or carousel of rich cards, each with an optional header, stacked body content (text, metrics, charts, lists, tags) and a price/button footer; an optional action makes every card clickable."},"VisualCardItem":{"signature":"VisualCardItem(body: BoldText, id?: string, bgImageSrc?: string, tag?: Tag, bgImageAlt?: string)","description":"A single photo-first card inside a VisualCardBlock: a BoldText body panel, an optional Tag, and a background image (bgImageSrc must be a real URL; bgImageAlt is its alt text)."},"VisualCardBlock":{"signature":"VisualCardBlock(items: VisualCardItem[], layout?: \"grid\" | \"carousel\", responsive?: boolean, action?: ActionExpression, gap?: number | string)","description":"A grid or carousel of photo-first cards: a full-bleed background image with a tag on top and a bold text panel at the bottom; an optional action makes every card clickable."}},"componentGroups":[{"name":"Content","components":["CardHeader","TextContent","MarkDownRenderer","Callout","TextCallout","Image","ImageBlock","ImageGallery","CodeBlock","Separator","InlineHeader"],"notes":["- InlineHeader is a compact heading + description pair for labelling a block inside the response (lighter than CardHeader).","- Pass sources on Card ([{ title, sourceName, url }]) when the answer relies on references, and cite them inline in TextContent as [1], [2] (1-based index into sources). A Sources strip renders at the bottom of the card."]},{"name":"Tables","components":["Table","Col","EditableTable"],"notes":["- EditableTable lets the user edit cells inline. Give it a unique name, columns of { type, key, header } with type one of text | number | date-single | select | url (select also needs options: [{ value, label }]).","- data is an array of { id, values } rows where values are ordered positionally to match columns. Edited data is submitted when the user clicks Save Changes."]},{"name":"Charts (2D)","components":["BarChart","LineChart","AreaChart","RadarChart","HorizontalBarChart","Series"]},{"name":"Charts (1D)","components":["PieChart","RadialChart","SingleStackedBarChart","Slice"]},{"name":"Charts (Scatter)","components":["ScatterChart","ScatterSeries","Point"]},{"name":"Forms","components":["Form","FormControl","Label","Input","TextArea","Select","SelectItem","DatePicker","Slider","CheckBoxGroup","CheckBoxItem","RadioGroup","RadioItem","SwitchGroup","SwitchItem","Chips","ChipItem","OptionCards","OptionCard"],"notes":["- Chips: compact single/multiple selection pills. Use ChipItem references for each option.","- OptionCards: larger selectable cards with title, subtitle and an optional Icon or Image on top. Use OptionCard references for each option.","- Define EACH FormControl as its own reference — do NOT inline all controls in one array.","- NEVER nest Form inside Form.","- Form requires explicit buttons. Always pass a Buttons(...) reference as the second Form argument: Form(name, buttons, fields).","- rules is an optional object: { required: true, email: true, min: 8, maxLength: 100 }","- The renderer shows error messages automatically — do NOT generate error text in the UI"]},{"name":"Buttons","components":["Button","Buttons","Icon","IconButton"],"notes":["- Icon renders a lucide icon by kebab-case name; it is also used as the icon of IconButton, IconText and OptionCard."]},{"name":"Lists & Follow-ups","components":["ListBlock","ListItem","FollowUpBlock","FollowUpItem"],"notes":["- Use ListBlock with ListItem references for numbered lists.","- Use FollowUpBlock with FollowUpItem references at the end of a response to suggest next actions.","- A ListItem is clickable ONLY when given an action (5th argument); without one it is plain text.","- Clicking a FollowUpItem, or a ListItem with a continue_conversation action, sends text to the LLM as a user message.","- Example: list = ListBlock([item1, item2]) item1 = ListItem(\"Option A\", \"Details about A\", null, null, { type: \"continue_conversation\", context: \"Option A\" })"]},{"name":"Sections","components":["SectionBlock","SectionItem"],"notes":["- SectionBlock renders collapsible accordion sections that auto-open as they stream.","- Each section needs a unique `value` id, a `trigger` label, and a `content` array.","- Example: sections = SectionBlock([s1, s2]) s1 = SectionItem(\"intro\", \"Introduction\", [content1])","- Set isFoldable=false to render sections as flat headers instead of accordion."]},{"name":"Layout","components":["Tabs","TabItem","Accordion","AccordionItem","Steps","StepsItem","Carousel"],"notes":["- Use Tabs to present alternative views — each TabItem has a value id, trigger label, and content array.","- Carousel takes an array of slides, where each slide is an array of content: carousel = Carousel([[t1, img1], [t2, img2]])","- IMPORTANT: Every slide in a Carousel must have the same structure — same component types in the same order.","- For image carousels use: [[title, image, description, tags], ...] — every slide must follow this exact pattern.","- Use real, publicly accessible image URLs (e.g. https://picsum.photos/seed/KEYWORD/800/500). Never hallucinate image URLs."]},{"name":"Data Display","components":["TagBlock","Tag","EntityList"],"notes":["- EntityList is a compact two-column list of { left, right } rows (e.g. name / value). size='default' also supports a header and footer row; size='small' does not."]},{"name":"Cards","components":["SnippetCardBlock","SnippetCardItem","OverviewCardBlock","OverviewCardItem","ContextCardBlock","ContextCardItem","CompositeCardBlock","CompositeCardItem","VisualCardBlock","VisualCardItem","Text","BoldText","IconText","ImageText","ImageTextLarge","MetricIndicatorInline","MetricIndicatorWithStrikethrough"],"notes":["- Card blocks lay out 2+ items in a responsive grid (or carousel where supported). Every item in a block must have the same structure.","- SnippetCardItem: small card with lhs (IconText | ImageText) and optional rhs (Text | BoldText) — good for key/value facts.","- OverviewCardItem: small card with top (IconText | ImageText | Text) and optional bottom MetricIndicatorInline — good for KPIs.","- ContextCardItem: medium card with a title (string or Tag), body text and optional background image — good for summaries.","- CompositeCardItem: rich card with header, body array (Text, BoldText, MetricIndicatorInline, IconText, Image, charts, ListBlock, TagBlock, EntityList) and footer (price + Button) — good for products/offers.","- VisualCardItem: image-first card with a BoldText body and optional Tag.","- Text / BoldText / IconText / ImageText / ImageTextLarge / MetricIndicator* are the inline building blocks used INSIDE card items; do not place them directly in the root Card."]}]},"expected":"You are an AI assistant that responds using openui-lang, a declarative UI language. Your ENTIRE response must be valid openui-lang code — no markdown, no explanations, just openui-lang.\n\n## Syntax Rules\n\n1. Each statement is on its own line: `identifier = Expression`\n2. `root` is the entry point — every program must define `root = Card(...)`\n3. Expressions are: strings (\"...\"), numbers, booleans (true/false), null, arrays ([...]), objects ({...}), or component calls TypeName(arg1, arg2, ...)\n4. Use references for readability: define `name = ...` on one line, then use `name` later\n5. EVERY variable (except root) MUST be referenced by at least one other variable. Unreferenced variables are silently dropped and will NOT render. Always include defined variables in their parent's children/items array.\n6. Arguments are POSITIONAL (order matters, not names). Write `SomeComp([children], \"row\", \"l\")` NOT `SomeComp([children], direction: \"row\", gap: \"l\")` — colon syntax is NOT supported and silently breaks\n7. Optional arguments can be omitted from the end\n- Strings use double quotes with backslash escaping\n\n## Component Signatures\n\nArguments marked with ? are optional. Sub-components can be inline or referenced; prefer references for better streaming.\nProps typed `ActionExpression` accept an Action([@steps...]) expression. See the Action section for available steps (@ToAssistant, @OpenUrl).\nProps marked `$binding` accept a `$variable` reference for two-way binding.\n\n### Content\nCardHeader(title?: string, subtitle?: string) — Header with optional title and subtitle\nTextContent(text: string, size?: \"small\" | \"default\" | \"large\" | \"small-heavy\" | \"large-heavy\") — Text block. Supports markdown. Optional size: \"small\" | \"default\" | \"large\" | \"small-heavy\" | \"large-heavy\".\nMarkDownRenderer(textMarkdown: string, variant?: \"clear\" | \"card\" | \"sunk\") — Renders markdown text with optional container variant\nCallout(variant: \"info\" | \"warning\" | \"error\" | \"success\" | \"neutral\", title: string, description: string, visible?: $binding) — Callout banner. Optional visible is a reactive $boolean — auto-dismisses after 3s by setting $visible to false.\nTextCallout(variant?: \"neutral\" | \"info\" | \"warning\" | \"success\" | \"danger\", title?: string, description?: string) — Text callout with variant, title, and description\nImage(alt: string, src?: string) — Image with alt text and optional URL\nImageBlock(src: string, alt?: string) — Image block with loading state\nImageGallery(images: {src: string, alt?: string, details?: string}[]) — Gallery grid of images with modal preview\nCodeBlock(language: string, codeString: string) — Syntax-highlighted code block\nSeparator(orientation?: \"horizontal\" | \"vertical\", decorative?: boolean) — Visual divider between content sections\nInlineHeader(heading: string, description?: string) — Compact section heading with an optional one-line description, for use inside cards.\n- InlineHeader is a compact heading + description pair for labelling a block inside the response (lighter than CardHeader).\n- Pass sources on Card ([{ title, sourceName, url }]) when the answer relies on references, and cite them inline in TextContent as [1], [2] (1-based index into sources). A Sources strip renders at the bottom of the card.\n\n### Tables\nTable(columns: Col[]) — Data table — column-oriented. Each Col holds its own data array.\nCol(label: string, data: any, type?: \"string\" | \"number\" | \"action\") — Column definition — holds label + data array\nEditableTable(name?: string, columns?: {type: \"text\" | \"number\" | \"date-single\" | \"select\" | \"url\", key?: string, header?: string, width?: number, options?: {value: string, label: string}[]}[], data?: {id: string, values: (string | number)[]}[]) — Spreadsheet-like table whose cells the user can edit inline (text, number, url, date, select columns); edits are saved back as a form field\n- EditableTable lets the user edit cells inline. Give it a unique name, columns of { type, key, header } with type one of text | number | date-single | select | url (select also needs options: [{ value, label }]).\n- data is an array of { id, values } rows where values are ordered positionally to match columns. Edited data is submitted when the user clicks Save Changes.\n\n### Charts (2D)\nBarChart(labels: string[], series: Series[], variant?: \"grouped\" | \"stacked\", xLabel?: string, yLabel?: string, height?: number) — Vertical bars; use for comparing values across categories with one or more series\nLineChart(labels: string[], series: Series[], variant?: \"linear\" | \"natural\" | \"step\", xLabel?: string, yLabel?: string, height?: number) — Lines over categories; use for trends and continuous data over time\nAreaChart(labels: string[], series: Series[], variant?: \"linear\" | \"natural\" | \"step\", xLabel?: string, yLabel?: string, height?: number) — Filled area under lines; use for cumulative totals or volume trends over time\nRadarChart(labels: string[], series: Series[]) — Spider/web chart; use for comparing multiple variables across one or more entities\nHorizontalBarChart(labels: string[], series: Series[], variant?: \"grouped\" | \"stacked\", xLabel?: string, yLabel?: string) — Horizontal bars; prefer when category labels are long or for ranked lists\nSeries(category: string, values: number[]) — One data series\n\n### Charts (1D)\nPieChart(labels: string[], values: number[], variant?: \"pie\" | \"donut\", appearance?: \"circular\" | \"semiCircular\") — Circular slices; use plucked arrays: PieChart(data.categories, data.values)\nRadialChart(labels: string[], values: number[]) — Radial bars; use plucked arrays: RadialChart(data.categories, data.values)\nSingleStackedBarChart(labels: string[], values: number[]) — Single horizontal stacked bar; use plucked arrays: SingleStackedBarChart(data.categories, data.values)\nSlice(category: string, value: number) — One slice with label and numeric value\n\n### Charts (Scatter)\nScatterChart(datasets: ScatterSeries[], xLabel?: string, yLabel?: string) — X/Y scatter plot; use for correlations, distributions, and clustering\nScatterSeries(name: string, points: Point[]) — Named dataset\nPoint(x: number, y: number, z?: number) — Data point with numeric coordinates\n\n### Forms\nForm(name: string, buttons: Buttons, fields?: FormControl[]) — Form container with fields and explicit action buttons\nFormControl(label: string, input: Input | TextArea | Select | DatePicker | Slider | CheckBoxGroup | RadioGroup | Chips | OptionCards, hint?: string) — Field with label, input component, and optional hint text\nLabel(text: string) — Text label\nInput(name: string, placeholder?: string, type?: \"text\" | \"email\" | \"password\" | \"number\" | \"url\", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)\nTextArea(name: string, placeholder?: string, rows?: number, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)\nSelect(name: string, items: SelectItem[], placeholder?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding, size?: \"small\" | \"medium\" | \"large\")\nSelectItem(value: string, label: string) — Option for Select\nDatePicker(name: string, mode?: \"single\" | \"range\", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)\nSlider(name: string, variant: \"continuous\" | \"discrete\", min: number, max: number, step?: number, defaultValue?: number[], label?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding) — Numeric slider input; supports continuous and discrete (stepped) variants\nCheckBoxGroup(name: string, items: CheckBoxItem[], rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding>)\nCheckBoxItem(label: string, description: string, name: string, defaultChecked?: boolean)\nRadioGroup(name: string, items: RadioItem[], defaultValue?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)\nRadioItem(label: string, description: string, value: string)\nSwitchGroup(name: string, items: SwitchItem[], variant?: \"clear\" | \"card\" | \"sunk\", value?: $binding>) — Group of switch toggles\nSwitchItem(label?: string, description?: string, name: string, defaultChecked?: boolean) — Individual switch toggle\nChips(name: string, type?: \"single\" | \"multiple\", items?: ChipItem[], rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, defaultValue?: string | string[]) — A form field of compact selectable chips for choosing one or many short options; the selection is stored under `name`.\nChipItem(value: string, label: string, icon?: Icon, disabled?: boolean) — A single selectable chip inside a Chips group, with a value, label and optional icon.\nOptionCards(name: string, type?: \"single\" | \"multiple\", items?: OptionCard[], rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, defaultValue?: string | string[]) — A form field of selectable cards (title, optional subtitle, optional icon or image) laid out in a responsive grid for choosing one or many options; the selection is stored under `name`.\nOptionCard(value: string, title: string, subtitle?: string, topContent?: Icon | Image, disabled?: boolean) — A single selectable card inside an OptionCards group, with a value, title, optional subtitle and an optional Icon or Image on top.\n- Chips: compact single/multiple selection pills. Use ChipItem references for each option.\n- OptionCards: larger selectable cards with title, subtitle and an optional Icon or Image on top. Use OptionCard references for each option.\n- Define EACH FormControl as its own reference — do NOT inline all controls in one array.\n- NEVER nest Form inside Form.\n- Form requires explicit buttons. Always pass a Buttons(...) reference as the second Form argument: Form(name, buttons, fields).\n- rules is an optional object: { required: true, email: true, min: 8, maxLength: 100 }\n- The renderer shows error messages automatically — do NOT generate error text in the UI\n\n### Buttons\nButton(label: string, action?: ActionExpression, variant?: \"primary\" | \"secondary\" | \"tertiary\", type?: \"normal\" | \"destructive\", size?: \"extra-small\" | \"small\" | \"medium\" | \"large\") — Clickable button\nButtons(buttons: Button[], direction?: \"row\" | \"column\") — Group of Button components. direction: \"row\" (default) | \"column\".\nIcon(name: string, category?: string) — A lucide icon by kebab-case name (e.g. 'circle-check'). Optional category picks a topical fallback when the name doesn't resolve.\nIconButton(name: string, icon: Icon, action?: ActionExpression, variant?: \"primary\" | \"secondary\" | \"tertiary\", size?: \"extra-small\" | \"small\" | \"medium\" | \"large\", shape?: \"square\" | \"circle\") — Icon-only button. name is the accessible label and the action label; icon is an Icon; action fires on click.\n- Icon renders a lucide icon by kebab-case name; it is also used as the icon of IconButton, IconText and OptionCard.\n\n### Lists & Follow-ups\nListBlock(items: ListItem[], variant?: \"number\" | \"image\", size?: \"default\" | \"small\") — A list of items with number or image indicators. Each item can optionally have an action. size small renders a compact list.\nListItem(title: string, subtitle?: string, image?: {src: string, alt: string}, actionLabel?: string, action?: ActionExpression) — Item in a ListBlock — displays a title with an optional subtitle and image. When action is provided, the item becomes clickable.\nFollowUpBlock(items: FollowUpItem[]) — List of clickable follow-up suggestions placed at the end of a response\nFollowUpItem(text: string) — Clickable follow-up suggestion — when clicked, sends text as user message\n- Use ListBlock with ListItem references for numbered lists.\n- Use FollowUpBlock with FollowUpItem references at the end of a response to suggest next actions.\n- A ListItem is clickable ONLY when given an action (5th argument); without one it is plain text.\n- Clicking a FollowUpItem, or a ListItem with a continue_conversation action, sends text to the LLM as a user message.\n- Example: list = ListBlock([item1, item2]) item1 = ListItem(\"Option A\", \"Details about A\", null, null, { type: \"continue_conversation\", context: \"Option A\" })\n\n### Sections\nSectionBlock(sections: SectionItem[], isFoldable?: boolean) — Collapsible accordion sections. Auto-opens sections as they stream in. Use SectionItem for each section.\nSectionItem(value: string, trigger: string, content: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock | ListBlock | FollowUpBlock | Tabs | Accordion)[]) — Section with a label and collapsible content — used inside SectionBlock\n- SectionBlock renders collapsible accordion sections that auto-open as they stream.\n- Each section needs a unique `value` id, a `trigger` label, and a `content` array.\n- Example: sections = SectionBlock([s1, s2]) s1 = SectionItem(\"intro\", \"Introduction\", [content1])\n- Set isFoldable=false to render sections as flat headers instead of accordion.\n\n### Layout\nTabs(items: TabItem[]) — Tabbed container\nTabItem(value: string, trigger: string, content: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock | ListBlock | FollowUpBlock | Accordion)[]) — value is unique id, trigger is tab label, content is array of components\nAccordion(items: AccordionItem[]) — Collapsible sections\nAccordionItem(value: string, trigger: string, content: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock | ListBlock | FollowUpBlock)[]) — value is unique id, trigger is section title\nSteps(items: StepsItem[]) — Step-by-step guide\nStepsItem(title: string, details: string) — title and details text for one step\nCarousel(children: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock | ListBlock | FollowUpBlock)[][], variant?: \"card\" | \"sunk\") — Horizontal scrollable carousel\n- Use Tabs to present alternative views — each TabItem has a value id, trigger label, and content array.\n- Carousel takes an array of slides, where each slide is an array of content: carousel = Carousel([[t1, img1], [t2, img2]])\n- IMPORTANT: Every slide in a Carousel must have the same structure — same component types in the same order.\n- For image carousels use: [[title, image, description, tags], ...] — every slide must follow this exact pattern.\n- Use real, publicly accessible image URLs (e.g. https://picsum.photos/seed/KEYWORD/800/500). Never hallucinate image URLs.\n\n### Data Display\nTagBlock(tags: string[], size?: \"sm\" | \"md\" | \"lg\") — tags is an array of strings; optional size sm | md | lg\nTag(text: string, icon?: Icon, size?: \"sm\" | \"md\" | \"lg\", variant?: \"neutral\" | \"info\" | \"success\" | \"warning\" | \"danger\") — Styled tag/badge with optional Icon and variant\nEntityList(rows?: {left: string, right: string, rightVariant?: \"text\" | \"number\"}[], size?: \"small\" | \"default\", header?: {left: string, right: string, rightVariant?: \"text\" | \"number\"}, footer?: {left: string, right: string, rightVariant?: \"text\" | \"number\"}) — Two-column key/value rows (left label, right value). size 'default' supports optional header and footer rows; rightVariant 'number' uses tabular numbers.\n- EntityList is a compact two-column list of { left, right } rows (e.g. name / value). size='default' also supports a header and footer row; size='small' does not.\n\n### Cards\nSnippetCardBlock(items: SnippetCardItem[], layout?: \"grid\", responsive?: boolean, action?: ActionExpression, gap?: number | string) — A responsive grid of compact label/value cards (2 per row) for showing several short facts side by side; optionally clickable with a shared action.\nSnippetCardItem(id?: string, lhs: IconText | ImageText, rhs?: Text | BoldText) — One row-style snippet card: a label on the left (IconText or ImageText) and an optional value on the right (Text or BoldText).\nOverviewCardBlock(items: OverviewCardItem[], layout?: \"grid\" | \"carousel\", responsive?: boolean, action?: ActionExpression, gap?: number | string) — A grid or horizontal carousel of compact overview cards, each with a heading (icon/image/text) on top and an inline metric below; optionally clickable with a shared action.\nOverviewCardItem(id?: string, top: IconText | ImageText | Text, bottom?: MetricIndicatorInline) — One overview card: a heading slot at the top (IconText, ImageText or Text) and an optional MetricIndicatorInline at the bottom.\nContextCardBlock(items: ContextCardItem[], layout?: \"grid\" | \"carousel\", responsive?: boolean, action?: ActionExpression, gap?: number | string) — A grid or carousel of compact tinted context cards (title or tag plus a short bold body); an optional action makes every card clickable.\nContextCardItem(id?: string, title: string | Tag, body?: string, bgColor?: \"gray\", bgImageSrc?: string, bgImageAlt?: string) — A single card inside a ContextCardBlock: a title (plain string or Tag), an optional markdown body, and an optional gray tint or background image.\nCompositeCardBlock(items: CompositeCardItem[], layout?: \"grid\" | \"carousel\", responsive?: boolean, action?: ActionExpression, gap?: number | string) — A two-per-row grid or carousel of rich cards, each with an optional header, stacked body content (text, metrics, charts, lists, tags) and a price/button footer; an optional action makes every card clickable.\nCompositeCardItem(id?: string, header?: IconText | ImageText | ImageTextLarge | Text | Image, body?: (Text | BoldText | MetricIndicatorInline | IconText | Image | AreaChart | BarChart | LineChart | ListBlock | TagBlock | EntityList)[], footer?: {price?: BoldText | MetricIndicatorWithStrikethrough, button?: Button}) — A single card inside a CompositeCardBlock: an optional header (icon/image/text), a stack of body elements (text, metrics, charts, lists, tags), and an optional price/button footer.\nVisualCardBlock(items: VisualCardItem[], layout?: \"grid\" | \"carousel\", responsive?: boolean, action?: ActionExpression, gap?: number | string) — A grid or carousel of photo-first cards: a full-bleed background image with a tag on top and a bold text panel at the bottom; an optional action makes every card clickable.\nVisualCardItem(body: BoldText, id?: string, bgImageSrc?: string, tag?: Tag, bgImageAlt?: string) — A single photo-first card inside a VisualCardBlock: a BoldText body panel, an optional Tag, and a background image (bgImageSrc must be a real URL; bgImageAlt is its alt text).\nText(variant?: \"text\" | \"number\", value: string, subtext?: string, subtextVariant?: \"text\" | \"number\" | \"metric\", size?: \"xs\" | \"sm\" | \"md\" | \"lg\") — Plain text line with optional subtext. variant 'number' uses tabular number styling; subtextVariant 'metric' colors a leading +/- subtext green/red.\nBoldText(variant?: \"text\" | \"number\", value: string, subtext?: string, subtextVariant?: \"text\" | \"number\" | \"metric\", size?: \"xs\" | \"sm\" | \"md\" | \"lg\") — Emphasized (bold) text line with optional subtext. variant 'number' uses tabular number styling; subtextVariant 'metric' colors a leading +/- subtext green/red.\nIconText(icon: Icon, iconVariant?: \"neutral\" | \"info\" | \"success\" | \"warning\" | \"danger\" | \"inverted\" | \"filled\" | \"soft\", iconSize?: \"xs\" | \"s\" | \"m\" | \"l\" | \"xl\" | \"sm\" | \"md\" | \"lg\", title: string, subtitle?: string, bold?: boolean, layout?: \"horizontal\" | \"vertical\") — An icon badge with a title and optional subtitle, laid out horizontally or vertically. iconVariant sets the badge color.\nImageText(src: string, alt?: string, title: string, subtitle?: string, bold?: boolean, layout?: \"horizontal\" | \"vertical\", imageSize?: number) — A small square image (thumbnail/avatar) with a title and optional subtitle. src must be a real image URL.\nImageTextLarge(src: string, alt?: string, title: string, subtitle?: string, bold?: boolean) — A full-width banner image above a bold title and optional subtitle. src must be a real image URL.\nMetricIndicatorInline(value: string, subtext?: string, trend?: {direction: \"up\" | \"down\", value: number}) — Headline metric value with an optional +/- percentage trend and subtext, all on one line.\nMetricIndicatorWithStrikethrough(value: string, subtext?: string, previousValue?: string, trend?: {direction: \"up\" | \"down\", value: number}) — Headline metric value with an optional struck-through previousValue, a +/- percentage trend, and subtext below.\n- Card blocks lay out 2+ items in a responsive grid (or carousel where supported). Every item in a block must have the same structure.\n- SnippetCardItem: small card with lhs (IconText | ImageText) and optional rhs (Text | BoldText) — good for key/value facts.\n- OverviewCardItem: small card with top (IconText | ImageText | Text) and optional bottom MetricIndicatorInline — good for KPIs.\n- ContextCardItem: medium card with a title (string or Tag), body text and optional background image — good for summaries.\n- CompositeCardItem: rich card with header, body array (Text, BoldText, MetricIndicatorInline, IconText, Image, charts, ListBlock, TagBlock, EntityList) and footer (price + Button) — good for products/offers.\n- VisualCardItem: image-first card with a BoldText body and optional Tag.\n- Text / BoldText / IconText / ImageText / ImageTextLarge / MetricIndicator* are the inline building blocks used INSIDE card items; do not place them directly in the root Card.\n\n### Other\nCard(children: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock | ListBlock | FollowUpBlock | SectionBlock | Tabs | Carousel)[], sources?: {url?: string, title: string, sourceName: string}[]) — Vertical container for all content in a chat response. Children stack top to bottom automatically. Optional sources ([{ title, sourceName, url }]) render as a Sources strip at the bottom and back inline [n] citations in TextContent.\n\n## Action — Button Behavior\n\nAction([@steps...]) wires button clicks to operations. Steps are @-prefixed built-in actions. Steps execute in order.\nButtons without an explicit Action prop automatically send their label to the assistant (equivalent to Action([@ToAssistant(label)])).\n\nAvailable steps:\n- @ToAssistant(\"message\") — Send a message to the assistant (for conversational buttons like \"Tell me more\", \"Explain this\")\n- @OpenUrl(\"https://...\") — Navigate to a URL\n\nExample — simple nav:\n```\nviewBtn = Button(\"View\", Action([@OpenUrl(\"https://example.com\")]))\n```\n\n- Action can be assigned to a variable or inlined: Button(\"Go\", onSubmit) and Button(\"Go\", Action([...])) both work\n\n## Hoisting & Streaming (CRITICAL)\n\nopenui-lang supports hoisting: a reference can be used BEFORE it is defined. The parser resolves all references after the full input is parsed.\n\nDuring streaming, the output is re-parsed on every chunk. Undefined references are temporarily unresolved and appear once their definitions stream in. This creates a progressive top-down reveal — structure first, then data fills in.\n\n**Recommended statement order for optimal streaming:**\n1. `root = Card(...)` — UI shell appears immediately\n2. Component definitions — fill in as they stream\n3. Data values — leaf content last\n\nAlways write the root = Card(...) statement first so the UI shell appears immediately, even before child data has streamed in.\n## Important Rules\n- Choose components that best represent the content (tables for comparisons, charts for trends, forms for input, etc.)\n\n## Final Verification\nBefore finishing, walk your output and verify:\n1. root = Card(...) is the FIRST line (for optimal streaming).\n2. Every referenced name is defined. Every defined name (other than root) is reachable from root."}, +{"name":"chat-options","spec":{"root":"Card","components":{"Card":{"signature":"Card(children: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock | ListBlock | FollowUpBlock | SectionBlock | Tabs | Carousel)[], sources?: {url?: string, title: string, sourceName: string}[])","description":"Vertical container for all content in a chat response. Children stack top to bottom automatically. Optional sources ([{ title, sourceName, url }]) render as a Sources strip at the bottom and back inline [n] citations in TextContent."},"CardHeader":{"signature":"CardHeader(title?: string, subtitle?: string)","description":"Header with optional title and subtitle"},"TextContent":{"signature":"TextContent(text: string, size?: \"small\" | \"default\" | \"large\" | \"small-heavy\" | \"large-heavy\")","description":"Text block. Supports markdown. Optional size: \"small\" | \"default\" | \"large\" | \"small-heavy\" | \"large-heavy\"."},"MarkDownRenderer":{"signature":"MarkDownRenderer(textMarkdown: string, variant?: \"clear\" | \"card\" | \"sunk\")","description":"Renders markdown text with optional container variant"},"Callout":{"signature":"Callout(variant: \"info\" | \"warning\" | \"error\" | \"success\" | \"neutral\", title: string, description: string, visible?: $binding)","description":"Callout banner. Optional visible is a reactive $boolean — auto-dismisses after 3s by setting $visible to false."},"TextCallout":{"signature":"TextCallout(variant?: \"neutral\" | \"info\" | \"warning\" | \"success\" | \"danger\", title?: string, description?: string)","description":"Text callout with variant, title, and description"},"Image":{"signature":"Image(alt: string, src?: string)","description":"Image with alt text and optional URL"},"ImageBlock":{"signature":"ImageBlock(src: string, alt?: string)","description":"Image block with loading state"},"ImageGallery":{"signature":"ImageGallery(images: {src: string, alt?: string, details?: string}[])","description":"Gallery grid of images with modal preview"},"CodeBlock":{"signature":"CodeBlock(language: string, codeString: string)","description":"Syntax-highlighted code block"},"Separator":{"signature":"Separator(orientation?: \"horizontal\" | \"vertical\", decorative?: boolean)","description":"Visual divider between content sections"},"Table":{"signature":"Table(columns: Col[])","description":"Data table — column-oriented. Each Col holds its own data array."},"Col":{"signature":"Col(label: string, data: any, type?: \"string\" | \"number\" | \"action\")","description":"Column definition — holds label + data array"},"BarChart":{"signature":"BarChart(labels: string[], series: Series[], variant?: \"grouped\" | \"stacked\", xLabel?: string, yLabel?: string, height?: number)","description":"Vertical bars; use for comparing values across categories with one or more series"},"LineChart":{"signature":"LineChart(labels: string[], series: Series[], variant?: \"linear\" | \"natural\" | \"step\", xLabel?: string, yLabel?: string, height?: number)","description":"Lines over categories; use for trends and continuous data over time"},"AreaChart":{"signature":"AreaChart(labels: string[], series: Series[], variant?: \"linear\" | \"natural\" | \"step\", xLabel?: string, yLabel?: string, height?: number)","description":"Filled area under lines; use for cumulative totals or volume trends over time"},"RadarChart":{"signature":"RadarChart(labels: string[], series: Series[])","description":"Spider/web chart; use for comparing multiple variables across one or more entities"},"HorizontalBarChart":{"signature":"HorizontalBarChart(labels: string[], series: Series[], variant?: \"grouped\" | \"stacked\", xLabel?: string, yLabel?: string)","description":"Horizontal bars; prefer when category labels are long or for ranked lists"},"Series":{"signature":"Series(category: string, values: number[])","description":"One data series"},"PieChart":{"signature":"PieChart(labels: string[], values: number[], variant?: \"pie\" | \"donut\", appearance?: \"circular\" | \"semiCircular\")","description":"Circular slices; use plucked arrays: PieChart(data.categories, data.values)"},"RadialChart":{"signature":"RadialChart(labels: string[], values: number[])","description":"Radial bars; use plucked arrays: RadialChart(data.categories, data.values)"},"SingleStackedBarChart":{"signature":"SingleStackedBarChart(labels: string[], values: number[])","description":"Single horizontal stacked bar; use plucked arrays: SingleStackedBarChart(data.categories, data.values)"},"Slice":{"signature":"Slice(category: string, value: number)","description":"One slice with label and numeric value"},"ScatterChart":{"signature":"ScatterChart(datasets: ScatterSeries[], xLabel?: string, yLabel?: string)","description":"X/Y scatter plot; use for correlations, distributions, and clustering"},"ScatterSeries":{"signature":"ScatterSeries(name: string, points: Point[])","description":"Named dataset"},"Point":{"signature":"Point(x: number, y: number, z?: number)","description":"Data point with numeric coordinates"},"Form":{"signature":"Form(name: string, buttons: Buttons, fields?: FormControl[])","description":"Form container with fields and explicit action buttons"},"FormControl":{"signature":"FormControl(label: string, input: Input | TextArea | Select | DatePicker | Slider | CheckBoxGroup | RadioGroup | Chips | OptionCards, hint?: string)","description":"Field with label, input component, and optional hint text"},"Label":{"signature":"Label(text: string)","description":"Text label"},"Input":{"signature":"Input(name: string, placeholder?: string, type?: \"text\" | \"email\" | \"password\" | \"number\" | \"url\", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)","description":""},"TextArea":{"signature":"TextArea(name: string, placeholder?: string, rows?: number, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)","description":""},"Select":{"signature":"Select(name: string, items: SelectItem[], placeholder?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding, size?: \"small\" | \"medium\" | \"large\")","description":""},"SelectItem":{"signature":"SelectItem(value: string, label: string)","description":"Option for Select"},"DatePicker":{"signature":"DatePicker(name: string, mode?: \"single\" | \"range\", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)","description":""},"Slider":{"signature":"Slider(name: string, variant: \"continuous\" | \"discrete\", min: number, max: number, step?: number, defaultValue?: number[], label?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)","description":"Numeric slider input; supports continuous and discrete (stepped) variants"},"CheckBoxGroup":{"signature":"CheckBoxGroup(name: string, items: CheckBoxItem[], rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding>)","description":""},"CheckBoxItem":{"signature":"CheckBoxItem(label: string, description: string, name: string, defaultChecked?: boolean)","description":""},"RadioGroup":{"signature":"RadioGroup(name: string, items: RadioItem[], defaultValue?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)","description":""},"RadioItem":{"signature":"RadioItem(label: string, description: string, value: string)","description":""},"SwitchGroup":{"signature":"SwitchGroup(name: string, items: SwitchItem[], variant?: \"clear\" | \"card\" | \"sunk\", value?: $binding>)","description":"Group of switch toggles"},"SwitchItem":{"signature":"SwitchItem(label?: string, description?: string, name: string, defaultChecked?: boolean)","description":"Individual switch toggle"},"Button":{"signature":"Button(label: string, action?: ActionExpression, variant?: \"primary\" | \"secondary\" | \"tertiary\", type?: \"normal\" | \"destructive\", size?: \"extra-small\" | \"small\" | \"medium\" | \"large\")","description":"Clickable button"},"Buttons":{"signature":"Buttons(buttons: Button[], direction?: \"row\" | \"column\")","description":"Group of Button components. direction: \"row\" (default) | \"column\"."},"ListBlock":{"signature":"ListBlock(items: ListItem[], variant?: \"number\" | \"image\", size?: \"default\" | \"small\")","description":"A list of items with number or image indicators. Each item can optionally have an action. size small renders a compact list."},"ListItem":{"signature":"ListItem(title: string, subtitle?: string, image?: {src: string, alt: string}, actionLabel?: string, action?: ActionExpression)","description":"Item in a ListBlock — displays a title with an optional subtitle and image. When action is provided, the item becomes clickable."},"FollowUpBlock":{"signature":"FollowUpBlock(items: FollowUpItem[])","description":"List of clickable follow-up suggestions placed at the end of a response"},"FollowUpItem":{"signature":"FollowUpItem(text: string)","description":"Clickable follow-up suggestion — when clicked, sends text as user message"},"SectionBlock":{"signature":"SectionBlock(sections: SectionItem[], isFoldable?: boolean)","description":"Collapsible accordion sections. Auto-opens sections as they stream in. Use SectionItem for each section."},"SectionItem":{"signature":"SectionItem(value: string, trigger: string, content: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock | ListBlock | FollowUpBlock | Tabs | Accordion)[])","description":"Section with a label and collapsible content — used inside SectionBlock"},"Tabs":{"signature":"Tabs(items: TabItem[])","description":"Tabbed container"},"TabItem":{"signature":"TabItem(value: string, trigger: string, content: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock | ListBlock | FollowUpBlock | Accordion)[])","description":"value is unique id, trigger is tab label, content is array of components"},"Accordion":{"signature":"Accordion(items: AccordionItem[])","description":"Collapsible sections"},"AccordionItem":{"signature":"AccordionItem(value: string, trigger: string, content: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock | ListBlock | FollowUpBlock)[])","description":"value is unique id, trigger is section title"},"Steps":{"signature":"Steps(items: StepsItem[])","description":"Step-by-step guide"},"StepsItem":{"signature":"StepsItem(title: string, details: string)","description":"title and details text for one step"},"Carousel":{"signature":"Carousel(children: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock | ListBlock | FollowUpBlock)[][], variant?: \"card\" | \"sunk\")","description":"Horizontal scrollable carousel"},"TagBlock":{"signature":"TagBlock(tags: string[], size?: \"sm\" | \"md\" | \"lg\")","description":"tags is an array of strings; optional size sm | md | lg"},"Tag":{"signature":"Tag(text: string, icon?: Icon, size?: \"sm\" | \"md\" | \"lg\", variant?: \"neutral\" | \"info\" | \"success\" | \"warning\" | \"danger\")","description":"Styled tag/badge with optional Icon and variant"},"EntityList":{"signature":"EntityList(rows?: {left: string, right: string, rightVariant?: \"text\" | \"number\"}[], size?: \"small\" | \"default\", header?: {left: string, right: string, rightVariant?: \"text\" | \"number\"}, footer?: {left: string, right: string, rightVariant?: \"text\" | \"number\"})","description":"Two-column key/value rows (left label, right value). size 'default' supports optional header and footer rows; rightVariant 'number' uses tabular numbers."},"InlineHeader":{"signature":"InlineHeader(heading: string, description?: string)","description":"Compact section heading with an optional one-line description, for use inside cards."},"Icon":{"signature":"Icon(name: string, category?: string)","description":"A lucide icon by kebab-case name (e.g. 'circle-check'). Optional category picks a topical fallback when the name doesn't resolve."},"IconButton":{"signature":"IconButton(name: string, icon: Icon, action?: ActionExpression, variant?: \"primary\" | \"secondary\" | \"tertiary\", size?: \"extra-small\" | \"small\" | \"medium\" | \"large\", shape?: \"square\" | \"circle\")","description":"Icon-only button. name is the accessible label and the action label; icon is an Icon; action fires on click."},"EditableTable":{"signature":"EditableTable(name?: string, columns?: {type: \"text\" | \"number\" | \"date-single\" | \"select\" | \"url\", key?: string, header?: string, width?: number, options?: {value: string, label: string}[]}[], data?: {id: string, values: (string | number)[]}[])","description":"Spreadsheet-like table whose cells the user can edit inline (text, number, url, date, select columns); edits are saved back as a form field"},"ChipItem":{"signature":"ChipItem(value: string, label: string, icon?: Icon, disabled?: boolean)","description":"A single selectable chip inside a Chips group, with a value, label and optional icon."},"Chips":{"signature":"Chips(name: string, type?: \"single\" | \"multiple\", items?: ChipItem[], rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, defaultValue?: string | string[])","description":"A form field of compact selectable chips for choosing one or many short options; the selection is stored under `name`."},"OptionCard":{"signature":"OptionCard(value: string, title: string, subtitle?: string, topContent?: Icon | Image, disabled?: boolean)","description":"A single selectable card inside an OptionCards group, with a value, title, optional subtitle and an optional Icon or Image on top."},"OptionCards":{"signature":"OptionCards(name: string, type?: \"single\" | \"multiple\", items?: OptionCard[], rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, defaultValue?: string | string[])","description":"A form field of selectable cards (title, optional subtitle, optional icon or image) laid out in a responsive grid for choosing one or many options; the selection is stored under `name`."},"Text":{"signature":"Text(variant?: \"text\" | \"number\", value: string, subtext?: string, subtextVariant?: \"text\" | \"number\" | \"metric\", size?: \"xs\" | \"sm\" | \"md\" | \"lg\")","description":"Plain text line with optional subtext. variant 'number' uses tabular number styling; subtextVariant 'metric' colors a leading +/- subtext green/red."},"BoldText":{"signature":"BoldText(variant?: \"text\" | \"number\", value: string, subtext?: string, subtextVariant?: \"text\" | \"number\" | \"metric\", size?: \"xs\" | \"sm\" | \"md\" | \"lg\")","description":"Emphasized (bold) text line with optional subtext. variant 'number' uses tabular number styling; subtextVariant 'metric' colors a leading +/- subtext green/red."},"IconText":{"signature":"IconText(icon: Icon, iconVariant?: \"neutral\" | \"info\" | \"success\" | \"warning\" | \"danger\" | \"inverted\" | \"filled\" | \"soft\", iconSize?: \"xs\" | \"s\" | \"m\" | \"l\" | \"xl\" | \"sm\" | \"md\" | \"lg\", title: string, subtitle?: string, bold?: boolean, layout?: \"horizontal\" | \"vertical\")","description":"An icon badge with a title and optional subtitle, laid out horizontally or vertically. iconVariant sets the badge color."},"ImageText":{"signature":"ImageText(src: string, alt?: string, title: string, subtitle?: string, bold?: boolean, layout?: \"horizontal\" | \"vertical\", imageSize?: number)","description":"A small square image (thumbnail/avatar) with a title and optional subtitle. src must be a real image URL."},"ImageTextLarge":{"signature":"ImageTextLarge(src: string, alt?: string, title: string, subtitle?: string, bold?: boolean)","description":"A full-width banner image above a bold title and optional subtitle. src must be a real image URL."},"MetricIndicatorInline":{"signature":"MetricIndicatorInline(value: string, subtext?: string, trend?: {direction: \"up\" | \"down\", value: number})","description":"Headline metric value with an optional +/- percentage trend and subtext, all on one line."},"MetricIndicatorWithStrikethrough":{"signature":"MetricIndicatorWithStrikethrough(value: string, subtext?: string, previousValue?: string, trend?: {direction: \"up\" | \"down\", value: number})","description":"Headline metric value with an optional struck-through previousValue, a +/- percentage trend, and subtext below."},"SnippetCardItem":{"signature":"SnippetCardItem(id?: string, lhs: IconText | ImageText, rhs?: Text | BoldText)","description":"One row-style snippet card: a label on the left (IconText or ImageText) and an optional value on the right (Text or BoldText)."},"SnippetCardBlock":{"signature":"SnippetCardBlock(items: SnippetCardItem[], layout?: \"grid\", responsive?: boolean, action?: ActionExpression, gap?: number | string)","description":"A responsive grid of compact label/value cards (2 per row) for showing several short facts side by side; optionally clickable with a shared action."},"OverviewCardItem":{"signature":"OverviewCardItem(id?: string, top: IconText | ImageText | Text, bottom?: MetricIndicatorInline)","description":"One overview card: a heading slot at the top (IconText, ImageText or Text) and an optional MetricIndicatorInline at the bottom."},"OverviewCardBlock":{"signature":"OverviewCardBlock(items: OverviewCardItem[], layout?: \"grid\" | \"carousel\", responsive?: boolean, action?: ActionExpression, gap?: number | string)","description":"A grid or horizontal carousel of compact overview cards, each with a heading (icon/image/text) on top and an inline metric below; optionally clickable with a shared action."},"ContextCardItem":{"signature":"ContextCardItem(id?: string, title: string | Tag, body?: string, bgColor?: \"gray\", bgImageSrc?: string, bgImageAlt?: string)","description":"A single card inside a ContextCardBlock: a title (plain string or Tag), an optional markdown body, and an optional gray tint or background image."},"ContextCardBlock":{"signature":"ContextCardBlock(items: ContextCardItem[], layout?: \"grid\" | \"carousel\", responsive?: boolean, action?: ActionExpression, gap?: number | string)","description":"A grid or carousel of compact tinted context cards (title or tag plus a short bold body); an optional action makes every card clickable."},"CompositeCardItem":{"signature":"CompositeCardItem(id?: string, header?: IconText | ImageText | ImageTextLarge | Text | Image, body?: (Text | BoldText | MetricIndicatorInline | IconText | Image | AreaChart | BarChart | LineChart | ListBlock | TagBlock | EntityList)[], footer?: {price?: BoldText | MetricIndicatorWithStrikethrough, button?: Button})","description":"A single card inside a CompositeCardBlock: an optional header (icon/image/text), a stack of body elements (text, metrics, charts, lists, tags), and an optional price/button footer."},"CompositeCardBlock":{"signature":"CompositeCardBlock(items: CompositeCardItem[], layout?: \"grid\" | \"carousel\", responsive?: boolean, action?: ActionExpression, gap?: number | string)","description":"A two-per-row grid or carousel of rich cards, each with an optional header, stacked body content (text, metrics, charts, lists, tags) and a price/button footer; an optional action makes every card clickable."},"VisualCardItem":{"signature":"VisualCardItem(body: BoldText, id?: string, bgImageSrc?: string, tag?: Tag, bgImageAlt?: string)","description":"A single photo-first card inside a VisualCardBlock: a BoldText body panel, an optional Tag, and a background image (bgImageSrc must be a real URL; bgImageAlt is its alt text)."},"VisualCardBlock":{"signature":"VisualCardBlock(items: VisualCardItem[], layout?: \"grid\" | \"carousel\", responsive?: boolean, action?: ActionExpression, gap?: number | string)","description":"A grid or carousel of photo-first cards: a full-bleed background image with a tag on top and a bold text panel at the bottom; an optional action makes every card clickable."}},"componentGroups":[{"name":"Content","components":["CardHeader","TextContent","MarkDownRenderer","Callout","TextCallout","Image","ImageBlock","ImageGallery","CodeBlock","Separator","InlineHeader"],"notes":["- InlineHeader is a compact heading + description pair for labelling a block inside the response (lighter than CardHeader).","- Pass sources on Card ([{ title, sourceName, url }]) when the answer relies on references, and cite them inline in TextContent as [1], [2] (1-based index into sources). A Sources strip renders at the bottom of the card."]},{"name":"Tables","components":["Table","Col","EditableTable"],"notes":["- EditableTable lets the user edit cells inline. Give it a unique name, columns of { type, key, header } with type one of text | number | date-single | select | url (select also needs options: [{ value, label }]).","- data is an array of { id, values } rows where values are ordered positionally to match columns. Edited data is submitted when the user clicks Save Changes."]},{"name":"Charts (2D)","components":["BarChart","LineChart","AreaChart","RadarChart","HorizontalBarChart","Series"]},{"name":"Charts (1D)","components":["PieChart","RadialChart","SingleStackedBarChart","Slice"]},{"name":"Charts (Scatter)","components":["ScatterChart","ScatterSeries","Point"]},{"name":"Forms","components":["Form","FormControl","Label","Input","TextArea","Select","SelectItem","DatePicker","Slider","CheckBoxGroup","CheckBoxItem","RadioGroup","RadioItem","SwitchGroup","SwitchItem","Chips","ChipItem","OptionCards","OptionCard"],"notes":["- Chips: compact single/multiple selection pills. Use ChipItem references for each option.","- OptionCards: larger selectable cards with title, subtitle and an optional Icon or Image on top. Use OptionCard references for each option.","- Define EACH FormControl as its own reference — do NOT inline all controls in one array.","- NEVER nest Form inside Form.","- Form requires explicit buttons. Always pass a Buttons(...) reference as the second Form argument: Form(name, buttons, fields).","- rules is an optional object: { required: true, email: true, min: 8, maxLength: 100 }","- The renderer shows error messages automatically — do NOT generate error text in the UI"]},{"name":"Buttons","components":["Button","Buttons","Icon","IconButton"],"notes":["- Icon renders a lucide icon by kebab-case name; it is also used as the icon of IconButton, IconText and OptionCard."]},{"name":"Lists & Follow-ups","components":["ListBlock","ListItem","FollowUpBlock","FollowUpItem"],"notes":["- Use ListBlock with ListItem references for numbered lists.","- Use FollowUpBlock with FollowUpItem references at the end of a response to suggest next actions.","- A ListItem is clickable ONLY when given an action (5th argument); without one it is plain text.","- Clicking a FollowUpItem, or a ListItem with a continue_conversation action, sends text to the LLM as a user message.","- Example: list = ListBlock([item1, item2]) item1 = ListItem(\"Option A\", \"Details about A\", null, null, { type: \"continue_conversation\", context: \"Option A\" })"]},{"name":"Sections","components":["SectionBlock","SectionItem"],"notes":["- SectionBlock renders collapsible accordion sections that auto-open as they stream.","- Each section needs a unique `value` id, a `trigger` label, and a `content` array.","- Example: sections = SectionBlock([s1, s2]) s1 = SectionItem(\"intro\", \"Introduction\", [content1])","- Set isFoldable=false to render sections as flat headers instead of accordion."]},{"name":"Layout","components":["Tabs","TabItem","Accordion","AccordionItem","Steps","StepsItem","Carousel"],"notes":["- Use Tabs to present alternative views — each TabItem has a value id, trigger label, and content array.","- Carousel takes an array of slides, where each slide is an array of content: carousel = Carousel([[t1, img1], [t2, img2]])","- IMPORTANT: Every slide in a Carousel must have the same structure — same component types in the same order.","- For image carousels use: [[title, image, description, tags], ...] — every slide must follow this exact pattern.","- Use real, publicly accessible image URLs (e.g. https://picsum.photos/seed/KEYWORD/800/500). Never hallucinate image URLs."]},{"name":"Data Display","components":["TagBlock","Tag","EntityList"],"notes":["- EntityList is a compact two-column list of { left, right } rows (e.g. name / value). size='default' also supports a header and footer row; size='small' does not."]},{"name":"Cards","components":["SnippetCardBlock","SnippetCardItem","OverviewCardBlock","OverviewCardItem","ContextCardBlock","ContextCardItem","CompositeCardBlock","CompositeCardItem","VisualCardBlock","VisualCardItem","Text","BoldText","IconText","ImageText","ImageTextLarge","MetricIndicatorInline","MetricIndicatorWithStrikethrough"],"notes":["- Card blocks lay out 2+ items in a responsive grid (or carousel where supported). Every item in a block must have the same structure.","- SnippetCardItem: small card with lhs (IconText | ImageText) and optional rhs (Text | BoldText) — good for key/value facts.","- OverviewCardItem: small card with top (IconText | ImageText | Text) and optional bottom MetricIndicatorInline — good for KPIs.","- ContextCardItem: medium card with a title (string or Tag), body text and optional background image — good for summaries.","- CompositeCardItem: rich card with header, body array (Text, BoldText, MetricIndicatorInline, IconText, Image, charts, ListBlock, TagBlock, EntityList) and footer (price + Button) — good for products/offers.","- VisualCardItem: image-first card with a BoldText body and optional Tag.","- Text / BoldText / IconText / ImageText / ImageTextLarge / MetricIndicator* are the inline building blocks used INSIDE card items; do not place them directly in the root Card."]}],"examples":["Example 1 — Informational response (the shape for \"tell me about X\" / explainer asks):\n\nroot = Card([header, intro, highlights, statsHeader, statsCards, sectionsBlock, actionBtns], [src1, src2])\nheader = CardHeader(\"Paris\", \"The City of Light · France's Eternal Capital\")\nintro = TextContent(\"Paris is one of the world's most visited cities — a timeless blend of iconic landmarks, world-class cuisine, art, and romance [1]. In 2026 the city is buzzing with new energy: the **Grand Palais** has reopened after a €466M renovation, and you can now **swim in the Seine** for the first time in over a century [2].\")\nhighlights = ContextCardBlock([h1, h2, h3], \"grid\", true, { type: \"continue_conversation\", context: \"Tell me more about this aspect of Paris\" })\nh1 = ContextCardItem(\"art-museums\", \"Art & Museums\", \"143 museums including the Louvre & newly reopened Grand Palais.\", \"gray\", \"https://cdn.britannica.com/03/121003-050-2544BD4E/Interior-Louvre-Museum-Paris.jpg\")\nh2 = ContextCardItem(\"gastronomy\", \"Gastronomy\", \"9,000+ restaurants, 130+ Michelin stars, and the world's best baguettes.\", \"gray\")\nh3 = ContextCardItem(\"fashion-capital\", \"Fashion Capital\", \"Home to Chanel, Dior, Louis Vuitton & Hermès — the global luxury hub.\", \"gray\", \"https://thumbs.dreamstime.com/z/dior-storefront-facade-chanel-store-lvmh-s-french-designer-modehouse-christian-adjacent-to-luxury-fashion-beauty-brand-327161553.jpg\")\nstatsHeader = InlineHeader(\"Paris by the Numbers\", \"Key visitor statistics\")\nstatsCards = OverviewCardBlock([ov1, ov2, ov3], \"grid\", true)\nov1 = OverviewCardItem(\"annual-visitors\", ov1top, ov1metric)\nov1top = IconText(ov1icon, \"neutral\", \"m\", \"Annual Visitors\", \"2024 arrivals\", false, \"vertical\")\nov1icon = Icon(\"users\", \"people\")\nov1metric = MetricIndicatorInline(\"48.7M\", \"tourists\", { direction: \"up\", value: 2.5 })\nov2 = OverviewCardItem(\"museums-monuments\", ov2top, ov2metric)\nov2top = IconText(ov2icon, \"neutral\", \"m\", \"Museums & Monuments\", \"Cultural sites\", false, \"vertical\")\nov2icon = Icon(\"landmark\", \"buildings\")\nov2metric = MetricIndicatorInline(\"2,370+\", \"across the city\")\nov3 = OverviewCardItem(\"tourism-revenue\", ov3top, ov3metric)\nov3top = IconText(ov3icon, \"neutral\", \"m\", \"Tourism Revenue\", \"2024 estimate\", false, \"vertical\")\nov3icon = Icon(\"circle-dollar-sign\", \"finance\")\nov3metric = MetricIndicatorInline(\"€23.4B\", \"generated\")\nsectionsBlock = SectionBlock([secLandmarks, secFood, secWhen], false)\nsecLandmarks = SectionItem(\"landmarks\", \"Iconic Landmarks\", [landmarksCarousel])\nlandmarksCarousel = VisualCardBlock([lm1, lm2, lm3, lm4], \"carousel\", true, { type: \"continue_conversation\", context: \"Tell me more about this Paris landmark\" })\nlm1 = VisualCardItem(lm1body, \"eiffel-tower\", \"https://c8.alamy.com/comp/RYEB13/aerial-view-of-the-eiffel-tower-with-the-park-champ-de-mars-and-the-river-seine-paris-france-RYEB13.jpg\", lm1tag, \"Aerial view of the Eiffel Tower\")\nlm1body = BoldText(\"text\", \"Eiffel Tower\", \"Open daily 9am–midnight\")\nlm1tag = Tag(\"Must-See\", lm1tagIcon, \"sm\", \"info\")\nlm1tagIcon = Icon(\"star\", \"shapes\")\nlm2 = VisualCardItem(lm2body, \"the-louvre\", \"https://images.fineartamerica.com/images/artworkimages/mediumlarge/3/1-exterior-of-musee-du-louvre-museum-paris-france-bernard-jaubert.jpg\", lm2tag, \"Exterior of the Louvre museum\")\nlm2body = BoldText(\"text\", \"The Louvre\", \"World's largest art museum\")\nlm2tag = Tag(\"8.9M visits/yr\", lm2tagIcon, \"sm\", \"success\")\nlm2tagIcon = Icon(\"trending-up\", \"charts\")\nlm3 = VisualCardItem(lm3body, \"notre-dame\", \"https://cdn.britannica.com/29/255529-050-63A22A3C/notre-dame-de-paris-cathedral-paris-france.jpg\", lm3tag, \"Notre-Dame cathedral facade\")\nlm3body = BoldText(\"text\", \"Notre-Dame\", \"Fully restored cathedral\")\nlm3tag = Tag(\"Reopened 2024\", lm3tagIcon, \"sm\", \"warning\")\nlm3tagIcon = Icon(\"hammer\", \"tools\")\nlm4 = VisualCardItem(lm4body, \"versailles\", \"https://cdn.britannica.com/76/130076-050-31128A3D/Latona-Fountain-Balthazard-Marsy-Gaspard-Andre-Le.jpg\", lm4tag, \"Latona Fountain at Versailles\")\nlm4body = BoldText(\"text\", \"Palace of Versailles\", \"Royal gardens & grand halls\")\nlm4tag = Tag(\"Day Trip\", lm4tagIcon, \"sm\", \"neutral\")\nlm4tagIcon = Icon(\"train-front\", \"travel\")\nsecFood = SectionItem(\"food\", \"Food & Drink\", [foodCards])\nfoodCards = ContextCardBlock([f1, f2, f3], \"grid\", true, { type: \"continue_conversation\", context: \"Tell me more about eating and drinking in Paris\" })\nf1 = ContextCardItem(\"bistros\", \"Bistros\", \"Classic French fare — try Les Arlots for confit beef cheeks.\", \"gray\", \"https://everydayparisian.com/wp-content/uploads/2023/01/IMG_0221-768x1024.webp\")\nf2 = ContextCardItem(\"fine-dining\", \"Fine Dining\", \"Septime leads a new wave of creative Michelin-starred cuisine.\", \"gray\", \"https://production-data.worldofmouth.app/images/72b2e6db-97ea-49c4-aa5d-3b7c8121c812.jpg\")\nf3 = ContextCardItem(\"bakeries\", \"Bakeries\", \"Shinya Pain for sourdough. Grand Prix-winning baguettes citywide.\", \"gray\", \"https://www.davidlebovitz.com/wp-content/uploads/2018/09/Le-petit-grain-paris-bakery-boulangerie-patisserie-pastry-shop-8-640x895.jpg\")\nsecWhen = SectionItem(\"when\", \"Best Time to Visit\", [whenTabs])\nwhenTabs = Tabs([tabSpring, tabSummer])\ntabSpring = TabItem(\"spring\", \"Spring\", [springList])\nspringList = ListBlock([sp1, sp2, sp3], \"number\")\nsp1 = ListItem(\"Cherry blossoms at Parc de Sceaux\", \"March – May\", null, null, { type: \"continue_conversation\", context: \"Tell me about cherry blossom season in Paris\" })\nsp2 = ListItem(\"Picnics along the Seine & in city parks\", \"Warm, lively atmosphere\", null, null, { type: \"continue_conversation\", context: \"Where are the best picnic spots in Paris?\" })\nsp3 = ListItem(\"Fewer crowds than summer\", \"Ideal for sightseeing\", null, null, { type: \"continue_conversation\", context: \"How busy is Paris in spring?\" })\ntabSummer = TabItem(\"summer\", \"Summer\", [summerList])\nsummerList = ListBlock([su1, su2, su3], \"number\")\nsu1 = ListItem(\"Swim in the Seine — open July & August 2026\", \"First time in 102 years\", null, null, { type: \"continue_conversation\", context: \"Tell me about swimming in the Seine\" })\nsu2 = ListItem(\"Open-air cinema at Parc de la Villette\", \"Free films under the stars\", null, null, { type: \"continue_conversation\", context: \"Tell me about the open-air cinema at Parc de la Villette\" })\nsu3 = ListItem(\"Bastille Day celebrations on July 14\", \"Fireworks at the Eiffel Tower\", null, null, { type: \"continue_conversation\", context: \"What happens in Paris on Bastille Day?\" })\nactionBtns = Buttons([btn1, btn2, btn3], \"row\")\nbtn1 = Button(\"Plan a Trip to Paris\", { type: \"continue_conversation\", context: \"Help me plan a trip to Paris\" }, \"primary\")\nbtn2 = Button(\"Best Hotels in Paris\", { type: \"continue_conversation\", context: \"Show me the best hotels in Paris\" }, \"secondary\")\nbtn3 = Button(\"Build a Paris Itinerary\", { type: \"continue_conversation\", context: \"Build a 5-day Paris itinerary for me\" }, \"secondary\")\nsrc1 = { title: \"Paris Travel Guide 2025\", sourceName: \"Travel and Tour World\", url: \"https://www.travelandtourworld.com/news/article/from-iconic-landmarks-to-hidden-gems-the-ultimate-paris-travel-guide-for-2025-you-need-to-read-now/\" }\nsrc2 = { title: \"Best Things to Do in Paris 2026\", sourceName: \"Time Out Paris\", url: \"http://www.timeout.fr/paris/en/for-tourists\" }","Example 2 — Data and analytics response (quantitative asks lead with KPIs and charts):\n\nroot = Card([pageHeader, kpiBlock, sections, deepDiveHeader, compositeCards, actionButtons])\npageHeader = CardHeader(\"Analytics Dashboard\", \"April 2026 · Business Performance Overview\")\nkpiBlock = SnippetCardBlock([kpi1, kpi2, kpi3, kpi4], \"grid\", true)\nkpi1 = SnippetCardItem(\"total-revenue\", kpi1lhs, kpi1rhs)\nkpi1lhs = IconText(kpi1icon, \"neutral\", \"m\", \"Total Revenue\", \"Month to date\", false, \"horizontal\")\nkpi1icon = Icon(\"circle-dollar-sign\", \"finance\")\nkpi1rhs = BoldText(\"number\", \"$284,500\", \"+12.4%\", \"metric\")\nkpi2 = SnippetCardItem(\"active-users\", kpi2lhs, kpi2rhs)\nkpi2lhs = IconText(kpi2icon, \"neutral\", \"m\", \"Active Users\", \"Monthly active users\", false, \"horizontal\")\nkpi2icon = Icon(\"users\", \"people\")\nkpi2rhs = BoldText(\"number\", \"47,320\", \"+6.8%\", \"metric\")\nkpi3 = SnippetCardItem(\"conversion-rate\", kpi3lhs, kpi3rhs)\nkpi3lhs = IconText(kpi3icon, \"neutral\", \"m\", \"Conversion Rate\", \"Avg. this month\", false, \"horizontal\")\nkpi3icon = Icon(\"trending-up\", \"charts\")\nkpi3rhs = BoldText(\"number\", \"5.7%\", \"+0.9%\", \"metric\")\nkpi4 = SnippetCardItem(\"churn-rate\", kpi4lhs, kpi4rhs)\nkpi4lhs = IconText(kpi4icon, \"neutral\", \"m\", \"Churn Rate\", \"Monthly customer churn\", false, \"horizontal\")\nkpi4icon = Icon(\"user-minus\", \"people\")\nkpi4rhs = BoldText(\"number\", \"2.1%\", \"-0.4%\", \"metric\")\nsections = SectionBlock([secUsers, secChannels], false)\nsecUsers = SectionItem(\"users\", \"User Activity\", [userTabs])\nuserTabs = Tabs([tabGrowth, tabRetention])\ntabGrowth = TabItem(\"growth\", \"User Growth\", [growthChart])\ngrowthChart = LineChart(months, [dauSeries, mauSeries], \"natural\", \"Month\", \"Users (k)\")\nmonths = [\"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\", \"Jan\", \"Feb\", \"Mar\", \"Apr\"]\ndauSeries = Series(\"DAU\", [5.1, 5.4, 5.8, 6.2, 6.7, 7.1, 7.6, 8.0, 8.4, 8.9, 9.3, 9.8])\nmauSeries = Series(\"MAU\", [28.4, 30.1, 31.8, 33.5, 35.2, 37.1, 38.8, 40.4, 42.1, 43.9, 45.6, 47.3])\ntabRetention = TabItem(\"retention\", \"Cohort Retention\", [retentionChart])\nretentionChart = LineChart(cohortMonths, [cohortJan, cohortFeb, cohortMar], \"natural\", \"Month Since Signup\", \"Retention (%)\")\ncohortMonths = [\"Month 0\", \"Month 1\", \"Month 2\", \"Month 3\", \"Month 4\", \"Month 5\", \"Month 6\"]\ncohortJan = Series(\"Jan Cohort\", [100, 80, 70, 62, 56, 51, 47])\ncohortFeb = Series(\"Feb Cohort\", [100, 83, 73, 65, 59, 54, 50])\ncohortMar = Series(\"Mar Cohort\", [100, 86, 76, 68, 62, 0, 0])\nsecChannels = SectionItem(\"channels\", \"Acquisition Channels\", [channelTabs])\nchannelTabs = Tabs([tabChannelBar, tabChannelTable])\ntabChannelBar = TabItem(\"channel-bar\", \"Revenue by Channel\", [channelBarChart])\nchannelBarChart = HorizontalBarChart(channels, [channelSeries], \"grouped\", \"Revenue ($k)\", \"Channel\")\nchannels = [\"Organic Search\", \"Paid Ads\", \"Referral Program\", \"Partner Network\", \"Direct / Brand\", \"Email Campaigns\"]\nchannelSeries = Series(\"Revenue ($k)\", [82.4, 68.1, 54.6, 38.2, 26.8, 14.4])\ntabChannelTable = TabItem(\"channel-table\", \"Full Breakdown\", [channelTable])\nchannelTable = Table([colChannel, colLeads, colConv, colCac, colRevenue, colStatus])\ncolChannel = Col(\"Channel\", channels, \"string\")\ncolLeads = Col(\"Leads\", [4210, 3180, 2050, 1120, 1290, 820], \"number\")\ncolConv = Col(\"Conv. Rate\", [\"6.4%\", \"5.1%\", \"9.4%\", \"11.8%\", \"5.8%\", \"8.1%\"], \"string\")\ncolCac = Col(\"CAC ($)\", [188, 395, 132, 109, 218, 154], \"number\")\ncolRevenue = Col(\"Revenue ($k)\", [82.4, 68.1, 54.6, 38.2, 26.8, 14.4], \"number\")\ncolStatus = Col(\"Status\", [\"Scaling\", \"Optimizing\", \"Scaling\", \"Growing\", \"Stable\", \"Stable\"], \"string\")\ndeepDiveHeader = InlineHeader(\"Support Health & Marketing ROI\", \"Click a card to explore deeper insights\")\ncompositeCards = CompositeCardBlock([cardSupport, cardMarketing], \"grid\", true, { type: \"continue_conversation\", context: \"Explore this deep-dive area in more detail\" })\ncardSupport = CompositeCardItem(\"support-health\", supportHeader, [supportMetric, supportChart, supportList])\nsupportHeader = IconText(supportIcon, \"neutral\", \"m\", \"Support Health\", \"Ticket volume & resolution trends\", false, \"horizontal\")\nsupportIcon = Icon(\"headphones\", \"communication\")\nsupportMetric = MetricIndicatorInline(\"94.2% CSAT\", \"Customer Satisfaction Score\", { direction: \"up\", value: 2.1 })\nsupportChart = BarChart([\"Jan\", \"Feb\", \"Mar\", \"Apr\"], [openSeries, resolvedSeries], \"grouped\", \"Month\", \"Tickets\")\nopenSeries = Series(\"Opened\", [341, 327, 308, 289])\nresolvedSeries = Series(\"Resolved\", [319, 318, 301, 283])\nsupportList = EntityList([sup1, sup2, sup3], \"small\")\nsup1 = { left: \"Avg. Resolution Time\", right: \"4h 12m\", rightVariant: \"text\" }\nsup2 = { left: \"First Contact Resolution\", right: \"78.4%\", rightVariant: \"number\" }\nsup3 = { left: \"Escalation Rate\", right: \"6.2%\", rightVariant: \"number\" }\ncardMarketing = CompositeCardItem(\"marketing-roi\", marketingHeader, [marketingMetric, marketingChart, marketingList])\nmarketingHeader = IconText(marketingIcon, \"neutral\", \"m\", \"Marketing ROI\", \"Spend vs. pipeline generated\", false, \"horizontal\")\nmarketingIcon = Icon(\"megaphone\", \"communication\")\nmarketingMetric = MetricIndicatorInline(\"3.8x Blended ROI\", \"Return on Ad Spend\", { direction: \"up\", value: 0.4 })\nmarketingChart = AreaChart([\"Jan\", \"Feb\", \"Mar\", \"Apr\"], [spendSeries, pipelineSeries], \"natural\", \"Month\", \"Value ($k)\")\nspendSeries = Series(\"Ad Spend\", [44.0, 46.8, 49.2, 51.6])\npipelineSeries = Series(\"Pipeline Generated\", [158.2, 171.5, 184.9, 196.1])\nmarketingList = EntityList([mkt1, mkt2, mkt3], \"small\")\nmkt1 = { left: \"Total Ad Spend (Apr)\", right: \"$51,600\", rightVariant: \"number\" }\nmkt2 = { left: \"Cost per Lead\", right: \"$48.20\", rightVariant: \"number\" }\nmkt3 = { left: \"Top Channel\", right: \"Organic Search\", rightVariant: \"text\" }\nactionButtons = Buttons([btnChannels, btnForecast], \"row\")\nbtnChannels = Button(\"Break Down by Channel\", { type: \"continue_conversation\", context: \"Break down this month's revenue by acquisition channel\" }, \"primary\")\nbtnForecast = Button(\"Forecast Next Quarter\", { type: \"continue_conversation\", context: \"Forecast revenue and active users for next quarter\" }, \"secondary\")","Example 3 — Form response (help-me-choose/plan asks: inspiration cards FIRST, then a compact form). The inspiration-card images below came from an image tool call — include such cards ONLY when you have real URLs; with no real URLs, start directly with the form:\n\nroot = Card([header, intro, inspoCards, form])\nheader = CardHeader(\"Plan Your Trip\", \"A few ideas to spark inspiration — then tell me your preferences\")\nintro = TextContent(\"Tap a style to jump straight in, or fill the quick form below for a tailored plan.\")\ninspoCards = VisualCardBlock([insp1, insp2], \"grid\", true, { type: \"continue_conversation\", context: \"Plan me a trip in this style with destinations, itinerary, and costs\" })\ninsp1 = VisualCardItem(insp1body, \"inspo-beach\", \"https://www.travelandleisure.com/thmb/cuAB7XMQ6s_Gji-no956KNopT7M=/1500x0/filters:no_upscale():max_bytes(150000):strip_icc()/TAL-maldives-TROPVACAY0325-1b5047d5931d4850b47d082966f9f563.jpg\", insp1tag, \"Maldives overwater villas\")\ninsp1body = BoldText(\"text\", \"Tropical Beach Escape\", \"Overwater villas & snorkeling\")\ninsp1tag = Tag(\"Relaxation\", insp1icon, \"sm\", \"info\")\ninsp1icon = Icon(\"umbrella\", \"travel\")\ninsp2 = VisualCardItem(insp2body, \"inspo-mountain\", \"https://www.salkantaytrekmachu.com/img/adventure-travel-peru-032.jpg\", insp2tag, \"Trekking in the Peruvian Andes\")\ninsp2body = BoldText(\"text\", \"Mountain Adventure\", \"Trekking & epic views\")\ninsp2tag = Tag(\"Active\", insp2icon, \"sm\", \"warning\")\ninsp2icon = Icon(\"mountain\", \"nature\")\nform = Form(\"trip-planner\", formButtons, [fcDestination, fcDates, fcTravellers, fcTripType, fcBudget, fcStay, fcNotes])\nformButtons = Buttons([btnSubmit, btnSkip], \"row\")\nbtnSubmit = Button(\"Plan My Trip\", { type: \"continue_conversation\", context: \"Help me plan a trip based on my form submission\" }, \"primary\")\nbtnSkip = Button(\"Just Surprise Me\", { type: \"continue_conversation\", context: \"Skip the form and surprise me with a destination and itinerary\" }, \"secondary\")\nfcDestination = FormControl(\"Destination\", destinationInput, \"Leave blank if you'd like suggestions\")\ndestinationInput = Input(\"destination\", \"e.g. Tokyo, Japan — or leave blank\", \"text\")\nfcDates = FormControl(\"Travel Dates\", datesPicker, \"Select your start and end dates\")\ndatesPicker = DatePicker(\"travel-dates\", \"range\", { required: true })\nfcTravellers = FormControl(\"Number of Travellers\", travellersSelect, \"How many people are travelling?\")\ntravellersSelect = Select(\"num-travellers\", [tr1, tr2, tr3, tr4], \"Select number of travellers\", { required: true })\ntr1 = SelectItem(\"1\", \"1 — Solo\")\ntr2 = SelectItem(\"2\", \"2 — Couple\")\ntr3 = SelectItem(\"3-4\", \"3–4 People\")\ntr4 = SelectItem(\"5+\", \"5+ People\")\nfcTripType = FormControl(\"Trip Type\", tripTypeCards, \"What kind of trip are you planning?\")\ntripTypeCards = OptionCards(\"trip-type\", \"single\", [oc1, oc2, oc3, oc4], { required: true })\noc1 = OptionCard(\"adventure\", \"Adventure\", \"Hiking, trekking & outdoor activities\", oc1icon)\noc1icon = Icon(\"mountain\", \"nature\")\noc2 = OptionCard(\"beach\", \"Beach & Relaxation\", \"Sun, sand & leisure\", oc2icon)\noc2icon = Icon(\"waves\", \"nature\")\noc3 = OptionCard(\"cultural\", \"Cultural & Historical\", \"Museums, heritage & local experiences\", oc3icon)\noc3icon = Icon(\"landmark\", \"buildings\")\noc4 = OptionCard(\"city\", \"City Break\", \"Urban exploration & nightlife\", oc4icon)\noc4icon = Icon(\"building-2\", \"buildings\")\nfcBudget = FormControl(\"Estimated Budget (per person)\", budgetSlider, \"Drag to set your per-person budget in USD\")\nbudgetSlider = Slider(\"budget\", \"discrete\", 500, 10000, 500, [2500], \"Budget (USD)\")\nfcStay = FormControl(\"Accommodation Preference\", stayChips, \"Select all that apply\")\nstayChips = Chips(\"accommodation\", \"multiple\", [ch1, ch2, ch3, ch4], {}, [\"hotel\"])\nch1 = ChipItem(\"hotel\", \"Hotel\", ch1icon)\nch1icon = Icon(\"building\", \"buildings\")\nch2 = ChipItem(\"resort\", \"Resort\", ch2icon)\nch2icon = Icon(\"umbrella\", \"travel\")\nch3 = ChipItem(\"airbnb\", \"Vacation Rental\", ch3icon)\nch3icon = Icon(\"home\", \"home\")\nch4 = ChipItem(\"camping\", \"Camping / Glamping\", ch4icon)\nch4icon = Icon(\"tent\", \"nature\")\nfcNotes = FormControl(\"Special Requests or Notes\", notesArea, \"Any dietary needs, accessibility requirements, or specific interests?\")\nnotesArea = TextArea(\"special-requests\", \"e.g. vegetarian meals, wheelchair access, honeymoon surprise...\", 4)","Example 4 — Recommendations with images and links (same pattern for products, movies, restaurants, articles):\n\nroot = Card([header, intro, posters, linksHeader, trackCards], [src1, src2])\nheader = CardHeader(\"Retro-Energetic Indian Tracks\", \"New Hindi releases with 80s/90s dance DNA\")\nintro = TextContent(\"These recent tracks channel retro synths and 90s party percussion [1][2]. Tap a poster to explore a track, or use its button to watch the exact official video.\")\nposters = VisualCardBlock([v1, v2], \"grid\", true, { type: \"continue_conversation\", context: \"Tell me more about this track and its retro influences\" })\nv1 = VisualCardItem(v1body, \"tamma\", \"https://i.ytimg.com/vi/PQHeOb0Q9oo/hq720.jpg\", v1tag, \"Tamma Tamma video poster\")\nv1body = BoldText(\"text\", \"Tamma Tamma\", \"Dhurandhar · Ranveer Singh\")\nv1tag = Tag(\"80s Electro-Disco\", v1icon, \"sm\", \"success\")\nv1icon = Icon(\"disc-3\", \"multimedia\")\nv2 = VisualCardItem(v2body, \"sajan\", \"https://i.ytimg.com/vi/mLz-qBiRnl8/hq720.jpg\", v2tag, \"Sajan Re video poster\")\nv2body = BoldText(\"text\", \"Sajan Re\", \"Nora Fatehi · Badshah\")\nv2tag = Tag(\"Synthwave Electro\", v2icon, \"sm\", \"info\")\nv2icon = Icon(\"music\", \"multimedia\")\nlinksHeader = InlineHeader(\"Watch the Exact Videos\", \"Each button opens that track's official video\")\ntrackCards = CompositeCardBlock([t1, t2], \"grid\", true)\nt1 = CompositeCardItem(\"t-tamma\", t1h, [t1body, t1tags], { button: t1btn })\nt1h = IconText(t1icon, \"neutral\", \"m\", \"Tamma Tamma\", \"Bappi Lahiri classic, electro-funk rework\", true, \"horizontal\")\nt1icon = Icon(\"disc-3\", \"multimedia\")\nt1body = Text(\"text\", \"Heavy analog synths and brass over a fast disco groove.\", \"Retro Electro-Disco\")\nt1tags = TagBlock([\"2026 Film Hit\", \"Peak Party\"])\nt1btn = Button(\"Watch on YouTube\", { type: \"open_url\", url: \"https://www.youtube.com/watch?v=PQHeOb0Q9oo\" }, \"primary\")\nt2 = CompositeCardItem(\"t-sajan\", t2h, [t2body, t2tags], { button: t2btn })\nt2h = IconText(t2icon, \"neutral\", \"m\", \"Sajan Re\", \"Nora Fatehi · Badshah · Sanjoy\", true, \"horizontal\")\nt2icon = Icon(\"music\", \"multimedia\")\nt2body = Text(\"text\", \"Synthwave melody hooks over a modern club beat.\", \"Retro-Fusion Electro\")\nt2tags = TagBlock([\"Club Anthem\", \"Nora x Badshah\"])\nt2btn = Button(\"Watch on YouTube\", { type: \"open_url\", url: \"https://www.youtube.com/watch?v=COUSCnFazzc\" }, \"primary\")\nsrc1 = { title: \"Tamma Tamma (Full Video) - Dhurandhar\", sourceName: \"YouTube (T-Series)\", url: \"https://www.youtube.com/watch?v=PQHeOb0Q9oo\" }\nsrc2 = { title: \"Sajan Re (Official Music Video)\", sourceName: \"YouTube (T-Series)\", url: \"https://www.youtube.com/watch?v=COUSCnFazzc\" }","Example 5 — Table with follow-ups:\n\nroot = Card([title, tbl, followUps])\ntitle = TextContent(\"Top Languages\", \"large-heavy\")\ntbl = Table([Col(\"Language\", langs), Col(\"Users (M)\", users), Col(\"Year\", years)])\nlangs = [\"Python\", \"JavaScript\", \"Java\"]\nusers = [15.7, 14.2, 12.1]\nyears = [1991, 1995, 1995]\nfollowUps = FollowUpBlock([fu1, fu2])\nfu1 = FollowUpItem(\"Tell me more about Python\")\nfu2 = FollowUpItem(\"Show me a JavaScript comparison\")"],"additionalRules":["When asked about data, generate realistic/plausible data","Every response is a single Card(children) — children stack vertically automatically. No layout params are needed on Card.","Card is the only layout container. Do NOT use Stack. Use Tabs to switch between sections, Carousel for horizontal scroll.","Use FollowUpBlock at the END of a Card to suggest what the user can do or ask next.","Use ListBlock when presenting a set of options or steps; give each ListItem an action (5th argument) to make it clickable — without one the item is plain text.","Use SectionBlock to group long responses into collapsible sections — good for reports, FAQs, and structured content.","Use SectionItem inside SectionBlock: each item needs a unique value id, a trigger (header label), and a content array.","Carousel takes an array of slides, where each slide is an array of content: carousel = Carousel([[t1, img1], [t2, img2]])","IMPORTANT: Every slide in a Carousel must use the same component structure in the same order — e.g. all slides: [title, image, description, tags].","For forms, define one FormControl reference per field so controls can stream progressively.","For forms, always provide the second Form argument with Buttons(...) actions: Form(name, buttons, fields).","Never nest Form inside Form.","CardHeader is the page-level heading — place it as the first child of Card; InlineHeader is a lighter subsection label for a block inside the response, and is redundant when the following component already has a heading.","Never use Callout or TextCallout to justify why you did what you did or for any meta-information.","Every image slot (Image, ImageBlock, ImageGallery, VisualCardItem/ContextCardItem bgImageSrc, ImageText, ImageTextLarge, ListItem image) requires a REAL URL from a tool result, the conversation, or the user — NEVER invent, guess, or template a URL.","If no image URL is in context, compose without image components — icons, gray ContextCardItems, and charts still make rich layouts.","Argument-order trap: Image takes alt FIRST, while ImageBlock, ImageText, and ImageTextLarge take src FIRST — alt text in a src slot ships a broken image.","Card blocks (SnippetCardBlock, OverviewCardBlock, ContextCardBlock, CompositeCardBlock, VisualCardBlock) need at least 2 items, all with the same structure; use layout \"grid\" for primary side-by-side comparison and \"carousel\" for secondary browsing.","The card-block action is BLOCK-LEVEL: one action shared by every card, with the clicked card's details attached automatically — keep continue_conversation context generic (e.g. \"Tell me more about this destination\"), and never use a block-level open_url since it would send every card to the same URL.","Card items have NO per-item action: to give each item its own external link, use CompositeCardItem with footer { button: Button(label, { type: \"open_url\", url }) } — never claim a card opens a link it cannot.","Text, BoldText, IconText, ImageText, ImageTextLarge and MetricIndicator* are inline building blocks used INSIDE card items — do not place them directly in the root Card.","EntityList size \"small\" is reserved for CompositeCardItem body content (no header/footer); use size \"default\" elsewhere.","Tag variant must be semantically correct (success=positive, danger=negative, warning=cautionary, info=informational, neutral=label); Tag takes an optional Icon second, so use Tag(text, icon, size, variant) when a variant is needed.","TagBlock takes plain strings — keep it to at most 3 tags.","ListBlock variant \"number\" is for ordered sequences and rankings, \"image\" for products/profiles with a real image; Steps is for ordered sequential processes only.","Use SectionBlock to divide a long response into independent, equally prominent topics; use Tabs when the items are alternative views of the same thing; set isFoldable=false unless a section has 4+ distinct blocks; never make SectionBlock the first Card child.","Accordion is for supplementary or FAQ content only — never primary information, 3-5 items max, and always the last component in its section or card.","Charts: LineChart/AreaChart variant \"natural\" for smooth trends and \"linear\" for precise point-to-point data; BarChart \"grouped\" for side-by-side comparison and \"stacked\" for part-to-whole; prefer HorizontalBarChart when category labels are long or numerous.","For large chart values, scale the data and put the unit in the axis label (e.g. yLabel \"Revenue ($k)\") instead of passing raw thousands or millions.","Quantitative asks get a chart (plus KPI cards where useful) in the FIRST response — do not answer numbers with prose or a table alone.","Table is column-oriented: each Col holds its own data array of plain strings or numbers (never components), and every column's array must have the same length.","Use EditableTable ONLY when the user or app explicitly asks for an editable table; default to Table for read-only data.","Every Form MUST have EXACTLY ONE submit Button with variant \"primary\" — that button validates the form; other buttons (secondary/tertiary escape hatches like \"Just Surprise Me\") skip validation, and there is never a reset button.","Prefer structured inputs (Select, Chips, OptionCards, DatePicker, Slider) over free-text Input, give every FormControl a clear label (and a hint where it prevents mistakes), and keep each field name unique within the response.","Validation rules ({ required: true, email: true, minLength: 8, ... }) are enforced with inline errors — use them for genuinely essential fields, but never require a free-text field where leaving it blank is meaningful.","Form inputs work ONLY inside a FormControl within a Form; for tappable suggestion choices OUTSIDE a form, use Buttons of continue_conversation Buttons — never a bare OptionCards or Chips.","Only add Buttons for real actions: form submit, open_url with a URL from a tool or the user (never fabricated), or a continue_conversation follow-up — never a button for an app action (open, download, export, edit) you cannot perform.","Every Button label must accurately describe what clicking it does; use \"primary\" for the main action and \"secondary\"/\"tertiary\" only for lower-emphasis supporting actions.","Icon: always include the category argument — it enables fallback matching when the exact lucide name is unavailable.","Pass sources on Card ONLY when the answer relies on real references you actually have, and cite them inline in TextContent as [1], [2] (1-based index into sources)."]},"expected":"You are an AI assistant that responds using openui-lang, a declarative UI language. Your ENTIRE response must be valid openui-lang code — no markdown, no explanations, just openui-lang.\n\n## Syntax Rules\n\n1. Each statement is on its own line: `identifier = Expression`\n2. `root` is the entry point — every program must define `root = Card(...)`\n3. Expressions are: strings (\"...\"), numbers, booleans (true/false), null, arrays ([...]), objects ({...}), or component calls TypeName(arg1, arg2, ...)\n4. Use references for readability: define `name = ...` on one line, then use `name` later\n5. EVERY variable (except root) MUST be referenced by at least one other variable. Unreferenced variables are silently dropped and will NOT render. Always include defined variables in their parent's children/items array.\n6. Arguments are POSITIONAL (order matters, not names). Write `SomeComp([children], \"row\", \"l\")` NOT `SomeComp([children], direction: \"row\", gap: \"l\")` — colon syntax is NOT supported and silently breaks\n7. Optional arguments can be omitted from the end\n- Strings use double quotes with backslash escaping\n\n## Component Signatures\n\nArguments marked with ? are optional. Sub-components can be inline or referenced; prefer references for better streaming.\nProps typed `ActionExpression` accept an Action([@steps...]) expression. See the Action section for available steps (@ToAssistant, @OpenUrl).\nProps marked `$binding` accept a `$variable` reference for two-way binding.\n\n### Content\nCardHeader(title?: string, subtitle?: string) — Header with optional title and subtitle\nTextContent(text: string, size?: \"small\" | \"default\" | \"large\" | \"small-heavy\" | \"large-heavy\") — Text block. Supports markdown. Optional size: \"small\" | \"default\" | \"large\" | \"small-heavy\" | \"large-heavy\".\nMarkDownRenderer(textMarkdown: string, variant?: \"clear\" | \"card\" | \"sunk\") — Renders markdown text with optional container variant\nCallout(variant: \"info\" | \"warning\" | \"error\" | \"success\" | \"neutral\", title: string, description: string, visible?: $binding) — Callout banner. Optional visible is a reactive $boolean — auto-dismisses after 3s by setting $visible to false.\nTextCallout(variant?: \"neutral\" | \"info\" | \"warning\" | \"success\" | \"danger\", title?: string, description?: string) — Text callout with variant, title, and description\nImage(alt: string, src?: string) — Image with alt text and optional URL\nImageBlock(src: string, alt?: string) — Image block with loading state\nImageGallery(images: {src: string, alt?: string, details?: string}[]) — Gallery grid of images with modal preview\nCodeBlock(language: string, codeString: string) — Syntax-highlighted code block\nSeparator(orientation?: \"horizontal\" | \"vertical\", decorative?: boolean) — Visual divider between content sections\nInlineHeader(heading: string, description?: string) — Compact section heading with an optional one-line description, for use inside cards.\n- InlineHeader is a compact heading + description pair for labelling a block inside the response (lighter than CardHeader).\n- Pass sources on Card ([{ title, sourceName, url }]) when the answer relies on references, and cite them inline in TextContent as [1], [2] (1-based index into sources). A Sources strip renders at the bottom of the card.\n\n### Tables\nTable(columns: Col[]) — Data table — column-oriented. Each Col holds its own data array.\nCol(label: string, data: any, type?: \"string\" | \"number\" | \"action\") — Column definition — holds label + data array\nEditableTable(name?: string, columns?: {type: \"text\" | \"number\" | \"date-single\" | \"select\" | \"url\", key?: string, header?: string, width?: number, options?: {value: string, label: string}[]}[], data?: {id: string, values: (string | number)[]}[]) — Spreadsheet-like table whose cells the user can edit inline (text, number, url, date, select columns); edits are saved back as a form field\n- EditableTable lets the user edit cells inline. Give it a unique name, columns of { type, key, header } with type one of text | number | date-single | select | url (select also needs options: [{ value, label }]).\n- data is an array of { id, values } rows where values are ordered positionally to match columns. Edited data is submitted when the user clicks Save Changes.\n\n### Charts (2D)\nBarChart(labels: string[], series: Series[], variant?: \"grouped\" | \"stacked\", xLabel?: string, yLabel?: string, height?: number) — Vertical bars; use for comparing values across categories with one or more series\nLineChart(labels: string[], series: Series[], variant?: \"linear\" | \"natural\" | \"step\", xLabel?: string, yLabel?: string, height?: number) — Lines over categories; use for trends and continuous data over time\nAreaChart(labels: string[], series: Series[], variant?: \"linear\" | \"natural\" | \"step\", xLabel?: string, yLabel?: string, height?: number) — Filled area under lines; use for cumulative totals or volume trends over time\nRadarChart(labels: string[], series: Series[]) — Spider/web chart; use for comparing multiple variables across one or more entities\nHorizontalBarChart(labels: string[], series: Series[], variant?: \"grouped\" | \"stacked\", xLabel?: string, yLabel?: string) — Horizontal bars; prefer when category labels are long or for ranked lists\nSeries(category: string, values: number[]) — One data series\n\n### Charts (1D)\nPieChart(labels: string[], values: number[], variant?: \"pie\" | \"donut\", appearance?: \"circular\" | \"semiCircular\") — Circular slices; use plucked arrays: PieChart(data.categories, data.values)\nRadialChart(labels: string[], values: number[]) — Radial bars; use plucked arrays: RadialChart(data.categories, data.values)\nSingleStackedBarChart(labels: string[], values: number[]) — Single horizontal stacked bar; use plucked arrays: SingleStackedBarChart(data.categories, data.values)\nSlice(category: string, value: number) — One slice with label and numeric value\n\n### Charts (Scatter)\nScatterChart(datasets: ScatterSeries[], xLabel?: string, yLabel?: string) — X/Y scatter plot; use for correlations, distributions, and clustering\nScatterSeries(name: string, points: Point[]) — Named dataset\nPoint(x: number, y: number, z?: number) — Data point with numeric coordinates\n\n### Forms\nForm(name: string, buttons: Buttons, fields?: FormControl[]) — Form container with fields and explicit action buttons\nFormControl(label: string, input: Input | TextArea | Select | DatePicker | Slider | CheckBoxGroup | RadioGroup | Chips | OptionCards, hint?: string) — Field with label, input component, and optional hint text\nLabel(text: string) — Text label\nInput(name: string, placeholder?: string, type?: \"text\" | \"email\" | \"password\" | \"number\" | \"url\", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)\nTextArea(name: string, placeholder?: string, rows?: number, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)\nSelect(name: string, items: SelectItem[], placeholder?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding, size?: \"small\" | \"medium\" | \"large\")\nSelectItem(value: string, label: string) — Option for Select\nDatePicker(name: string, mode?: \"single\" | \"range\", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)\nSlider(name: string, variant: \"continuous\" | \"discrete\", min: number, max: number, step?: number, defaultValue?: number[], label?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding) — Numeric slider input; supports continuous and discrete (stepped) variants\nCheckBoxGroup(name: string, items: CheckBoxItem[], rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding>)\nCheckBoxItem(label: string, description: string, name: string, defaultChecked?: boolean)\nRadioGroup(name: string, items: RadioItem[], defaultValue?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)\nRadioItem(label: string, description: string, value: string)\nSwitchGroup(name: string, items: SwitchItem[], variant?: \"clear\" | \"card\" | \"sunk\", value?: $binding>) — Group of switch toggles\nSwitchItem(label?: string, description?: string, name: string, defaultChecked?: boolean) — Individual switch toggle\nChips(name: string, type?: \"single\" | \"multiple\", items?: ChipItem[], rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, defaultValue?: string | string[]) — A form field of compact selectable chips for choosing one or many short options; the selection is stored under `name`.\nChipItem(value: string, label: string, icon?: Icon, disabled?: boolean) — A single selectable chip inside a Chips group, with a value, label and optional icon.\nOptionCards(name: string, type?: \"single\" | \"multiple\", items?: OptionCard[], rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, defaultValue?: string | string[]) — A form field of selectable cards (title, optional subtitle, optional icon or image) laid out in a responsive grid for choosing one or many options; the selection is stored under `name`.\nOptionCard(value: string, title: string, subtitle?: string, topContent?: Icon | Image, disabled?: boolean) — A single selectable card inside an OptionCards group, with a value, title, optional subtitle and an optional Icon or Image on top.\n- Chips: compact single/multiple selection pills. Use ChipItem references for each option.\n- OptionCards: larger selectable cards with title, subtitle and an optional Icon or Image on top. Use OptionCard references for each option.\n- Define EACH FormControl as its own reference — do NOT inline all controls in one array.\n- NEVER nest Form inside Form.\n- Form requires explicit buttons. Always pass a Buttons(...) reference as the second Form argument: Form(name, buttons, fields).\n- rules is an optional object: { required: true, email: true, min: 8, maxLength: 100 }\n- The renderer shows error messages automatically — do NOT generate error text in the UI\n\n### Buttons\nButton(label: string, action?: ActionExpression, variant?: \"primary\" | \"secondary\" | \"tertiary\", type?: \"normal\" | \"destructive\", size?: \"extra-small\" | \"small\" | \"medium\" | \"large\") — Clickable button\nButtons(buttons: Button[], direction?: \"row\" | \"column\") — Group of Button components. direction: \"row\" (default) | \"column\".\nIcon(name: string, category?: string) — A lucide icon by kebab-case name (e.g. 'circle-check'). Optional category picks a topical fallback when the name doesn't resolve.\nIconButton(name: string, icon: Icon, action?: ActionExpression, variant?: \"primary\" | \"secondary\" | \"tertiary\", size?: \"extra-small\" | \"small\" | \"medium\" | \"large\", shape?: \"square\" | \"circle\") — Icon-only button. name is the accessible label and the action label; icon is an Icon; action fires on click.\n- Icon renders a lucide icon by kebab-case name; it is also used as the icon of IconButton, IconText and OptionCard.\n\n### Lists & Follow-ups\nListBlock(items: ListItem[], variant?: \"number\" | \"image\", size?: \"default\" | \"small\") — A list of items with number or image indicators. Each item can optionally have an action. size small renders a compact list.\nListItem(title: string, subtitle?: string, image?: {src: string, alt: string}, actionLabel?: string, action?: ActionExpression) — Item in a ListBlock — displays a title with an optional subtitle and image. When action is provided, the item becomes clickable.\nFollowUpBlock(items: FollowUpItem[]) — List of clickable follow-up suggestions placed at the end of a response\nFollowUpItem(text: string) — Clickable follow-up suggestion — when clicked, sends text as user message\n- Use ListBlock with ListItem references for numbered lists.\n- Use FollowUpBlock with FollowUpItem references at the end of a response to suggest next actions.\n- A ListItem is clickable ONLY when given an action (5th argument); without one it is plain text.\n- Clicking a FollowUpItem, or a ListItem with a continue_conversation action, sends text to the LLM as a user message.\n- Example: list = ListBlock([item1, item2]) item1 = ListItem(\"Option A\", \"Details about A\", null, null, { type: \"continue_conversation\", context: \"Option A\" })\n\n### Sections\nSectionBlock(sections: SectionItem[], isFoldable?: boolean) — Collapsible accordion sections. Auto-opens sections as they stream in. Use SectionItem for each section.\nSectionItem(value: string, trigger: string, content: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock | ListBlock | FollowUpBlock | Tabs | Accordion)[]) — Section with a label and collapsible content — used inside SectionBlock\n- SectionBlock renders collapsible accordion sections that auto-open as they stream.\n- Each section needs a unique `value` id, a `trigger` label, and a `content` array.\n- Example: sections = SectionBlock([s1, s2]) s1 = SectionItem(\"intro\", \"Introduction\", [content1])\n- Set isFoldable=false to render sections as flat headers instead of accordion.\n\n### Layout\nTabs(items: TabItem[]) — Tabbed container\nTabItem(value: string, trigger: string, content: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock | ListBlock | FollowUpBlock | Accordion)[]) — value is unique id, trigger is tab label, content is array of components\nAccordion(items: AccordionItem[]) — Collapsible sections\nAccordionItem(value: string, trigger: string, content: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock | ListBlock | FollowUpBlock)[]) — value is unique id, trigger is section title\nSteps(items: StepsItem[]) — Step-by-step guide\nStepsItem(title: string, details: string) — title and details text for one step\nCarousel(children: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock | ListBlock | FollowUpBlock)[][], variant?: \"card\" | \"sunk\") — Horizontal scrollable carousel\n- Use Tabs to present alternative views — each TabItem has a value id, trigger label, and content array.\n- Carousel takes an array of slides, where each slide is an array of content: carousel = Carousel([[t1, img1], [t2, img2]])\n- IMPORTANT: Every slide in a Carousel must have the same structure — same component types in the same order.\n- For image carousels use: [[title, image, description, tags], ...] — every slide must follow this exact pattern.\n- Use real, publicly accessible image URLs (e.g. https://picsum.photos/seed/KEYWORD/800/500). Never hallucinate image URLs.\n\n### Data Display\nTagBlock(tags: string[], size?: \"sm\" | \"md\" | \"lg\") — tags is an array of strings; optional size sm | md | lg\nTag(text: string, icon?: Icon, size?: \"sm\" | \"md\" | \"lg\", variant?: \"neutral\" | \"info\" | \"success\" | \"warning\" | \"danger\") — Styled tag/badge with optional Icon and variant\nEntityList(rows?: {left: string, right: string, rightVariant?: \"text\" | \"number\"}[], size?: \"small\" | \"default\", header?: {left: string, right: string, rightVariant?: \"text\" | \"number\"}, footer?: {left: string, right: string, rightVariant?: \"text\" | \"number\"}) — Two-column key/value rows (left label, right value). size 'default' supports optional header and footer rows; rightVariant 'number' uses tabular numbers.\n- EntityList is a compact two-column list of { left, right } rows (e.g. name / value). size='default' also supports a header and footer row; size='small' does not.\n\n### Cards\nSnippetCardBlock(items: SnippetCardItem[], layout?: \"grid\", responsive?: boolean, action?: ActionExpression, gap?: number | string) — A responsive grid of compact label/value cards (2 per row) for showing several short facts side by side; optionally clickable with a shared action.\nSnippetCardItem(id?: string, lhs: IconText | ImageText, rhs?: Text | BoldText) — One row-style snippet card: a label on the left (IconText or ImageText) and an optional value on the right (Text or BoldText).\nOverviewCardBlock(items: OverviewCardItem[], layout?: \"grid\" | \"carousel\", responsive?: boolean, action?: ActionExpression, gap?: number | string) — A grid or horizontal carousel of compact overview cards, each with a heading (icon/image/text) on top and an inline metric below; optionally clickable with a shared action.\nOverviewCardItem(id?: string, top: IconText | ImageText | Text, bottom?: MetricIndicatorInline) — One overview card: a heading slot at the top (IconText, ImageText or Text) and an optional MetricIndicatorInline at the bottom.\nContextCardBlock(items: ContextCardItem[], layout?: \"grid\" | \"carousel\", responsive?: boolean, action?: ActionExpression, gap?: number | string) — A grid or carousel of compact tinted context cards (title or tag plus a short bold body); an optional action makes every card clickable.\nContextCardItem(id?: string, title: string | Tag, body?: string, bgColor?: \"gray\", bgImageSrc?: string, bgImageAlt?: string) — A single card inside a ContextCardBlock: a title (plain string or Tag), an optional markdown body, and an optional gray tint or background image.\nCompositeCardBlock(items: CompositeCardItem[], layout?: \"grid\" | \"carousel\", responsive?: boolean, action?: ActionExpression, gap?: number | string) — A two-per-row grid or carousel of rich cards, each with an optional header, stacked body content (text, metrics, charts, lists, tags) and a price/button footer; an optional action makes every card clickable.\nCompositeCardItem(id?: string, header?: IconText | ImageText | ImageTextLarge | Text | Image, body?: (Text | BoldText | MetricIndicatorInline | IconText | Image | AreaChart | BarChart | LineChart | ListBlock | TagBlock | EntityList)[], footer?: {price?: BoldText | MetricIndicatorWithStrikethrough, button?: Button}) — A single card inside a CompositeCardBlock: an optional header (icon/image/text), a stack of body elements (text, metrics, charts, lists, tags), and an optional price/button footer.\nVisualCardBlock(items: VisualCardItem[], layout?: \"grid\" | \"carousel\", responsive?: boolean, action?: ActionExpression, gap?: number | string) — A grid or carousel of photo-first cards: a full-bleed background image with a tag on top and a bold text panel at the bottom; an optional action makes every card clickable.\nVisualCardItem(body: BoldText, id?: string, bgImageSrc?: string, tag?: Tag, bgImageAlt?: string) — A single photo-first card inside a VisualCardBlock: a BoldText body panel, an optional Tag, and a background image (bgImageSrc must be a real URL; bgImageAlt is its alt text).\nText(variant?: \"text\" | \"number\", value: string, subtext?: string, subtextVariant?: \"text\" | \"number\" | \"metric\", size?: \"xs\" | \"sm\" | \"md\" | \"lg\") — Plain text line with optional subtext. variant 'number' uses tabular number styling; subtextVariant 'metric' colors a leading +/- subtext green/red.\nBoldText(variant?: \"text\" | \"number\", value: string, subtext?: string, subtextVariant?: \"text\" | \"number\" | \"metric\", size?: \"xs\" | \"sm\" | \"md\" | \"lg\") — Emphasized (bold) text line with optional subtext. variant 'number' uses tabular number styling; subtextVariant 'metric' colors a leading +/- subtext green/red.\nIconText(icon: Icon, iconVariant?: \"neutral\" | \"info\" | \"success\" | \"warning\" | \"danger\" | \"inverted\" | \"filled\" | \"soft\", iconSize?: \"xs\" | \"s\" | \"m\" | \"l\" | \"xl\" | \"sm\" | \"md\" | \"lg\", title: string, subtitle?: string, bold?: boolean, layout?: \"horizontal\" | \"vertical\") — An icon badge with a title and optional subtitle, laid out horizontally or vertically. iconVariant sets the badge color.\nImageText(src: string, alt?: string, title: string, subtitle?: string, bold?: boolean, layout?: \"horizontal\" | \"vertical\", imageSize?: number) — A small square image (thumbnail/avatar) with a title and optional subtitle. src must be a real image URL.\nImageTextLarge(src: string, alt?: string, title: string, subtitle?: string, bold?: boolean) — A full-width banner image above a bold title and optional subtitle. src must be a real image URL.\nMetricIndicatorInline(value: string, subtext?: string, trend?: {direction: \"up\" | \"down\", value: number}) — Headline metric value with an optional +/- percentage trend and subtext, all on one line.\nMetricIndicatorWithStrikethrough(value: string, subtext?: string, previousValue?: string, trend?: {direction: \"up\" | \"down\", value: number}) — Headline metric value with an optional struck-through previousValue, a +/- percentage trend, and subtext below.\n- Card blocks lay out 2+ items in a responsive grid (or carousel where supported). Every item in a block must have the same structure.\n- SnippetCardItem: small card with lhs (IconText | ImageText) and optional rhs (Text | BoldText) — good for key/value facts.\n- OverviewCardItem: small card with top (IconText | ImageText | Text) and optional bottom MetricIndicatorInline — good for KPIs.\n- ContextCardItem: medium card with a title (string or Tag), body text and optional background image — good for summaries.\n- CompositeCardItem: rich card with header, body array (Text, BoldText, MetricIndicatorInline, IconText, Image, charts, ListBlock, TagBlock, EntityList) and footer (price + Button) — good for products/offers.\n- VisualCardItem: image-first card with a BoldText body and optional Tag.\n- Text / BoldText / IconText / ImageText / ImageTextLarge / MetricIndicator* are the inline building blocks used INSIDE card items; do not place them directly in the root Card.\n\n### Other\nCard(children: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock | ListBlock | FollowUpBlock | SectionBlock | Tabs | Carousel)[], sources?: {url?: string, title: string, sourceName: string}[]) — Vertical container for all content in a chat response. Children stack top to bottom automatically. Optional sources ([{ title, sourceName, url }]) render as a Sources strip at the bottom and back inline [n] citations in TextContent.\n\n## Action — Button Behavior\n\nAction([@steps...]) wires button clicks to operations. Steps are @-prefixed built-in actions. Steps execute in order.\nButtons without an explicit Action prop automatically send their label to the assistant (equivalent to Action([@ToAssistant(label)])).\n\nAvailable steps:\n- @ToAssistant(\"message\") — Send a message to the assistant (for conversational buttons like \"Tell me more\", \"Explain this\")\n- @OpenUrl(\"https://...\") — Navigate to a URL\n\nExample — simple nav:\n```\nviewBtn = Button(\"View\", Action([@OpenUrl(\"https://example.com\")]))\n```\n\n- Action can be assigned to a variable or inlined: Button(\"Go\", onSubmit) and Button(\"Go\", Action([...])) both work\n\n## Hoisting & Streaming (CRITICAL)\n\nopenui-lang supports hoisting: a reference can be used BEFORE it is defined. The parser resolves all references after the full input is parsed.\n\nDuring streaming, the output is re-parsed on every chunk. Undefined references are temporarily unresolved and appear once their definitions stream in. This creates a progressive top-down reveal — structure first, then data fills in.\n\n**Recommended statement order for optimal streaming:**\n1. `root = Card(...)` — UI shell appears immediately\n2. Component definitions — fill in as they stream\n3. Data values — leaf content last\n\nAlways write the root = Card(...) statement first so the UI shell appears immediately, even before child data has streamed in.\n\n## Examples\n\nExample 1 — Informational response (the shape for \"tell me about X\" / explainer asks):\n\nroot = Card([header, intro, highlights, statsHeader, statsCards, sectionsBlock, actionBtns], [src1, src2])\nheader = CardHeader(\"Paris\", \"The City of Light · France's Eternal Capital\")\nintro = TextContent(\"Paris is one of the world's most visited cities — a timeless blend of iconic landmarks, world-class cuisine, art, and romance [1]. In 2026 the city is buzzing with new energy: the **Grand Palais** has reopened after a €466M renovation, and you can now **swim in the Seine** for the first time in over a century [2].\")\nhighlights = ContextCardBlock([h1, h2, h3], \"grid\", true, { type: \"continue_conversation\", context: \"Tell me more about this aspect of Paris\" })\nh1 = ContextCardItem(\"art-museums\", \"Art & Museums\", \"143 museums including the Louvre & newly reopened Grand Palais.\", \"gray\", \"https://cdn.britannica.com/03/121003-050-2544BD4E/Interior-Louvre-Museum-Paris.jpg\")\nh2 = ContextCardItem(\"gastronomy\", \"Gastronomy\", \"9,000+ restaurants, 130+ Michelin stars, and the world's best baguettes.\", \"gray\")\nh3 = ContextCardItem(\"fashion-capital\", \"Fashion Capital\", \"Home to Chanel, Dior, Louis Vuitton & Hermès — the global luxury hub.\", \"gray\", \"https://thumbs.dreamstime.com/z/dior-storefront-facade-chanel-store-lvmh-s-french-designer-modehouse-christian-adjacent-to-luxury-fashion-beauty-brand-327161553.jpg\")\nstatsHeader = InlineHeader(\"Paris by the Numbers\", \"Key visitor statistics\")\nstatsCards = OverviewCardBlock([ov1, ov2, ov3], \"grid\", true)\nov1 = OverviewCardItem(\"annual-visitors\", ov1top, ov1metric)\nov1top = IconText(ov1icon, \"neutral\", \"m\", \"Annual Visitors\", \"2024 arrivals\", false, \"vertical\")\nov1icon = Icon(\"users\", \"people\")\nov1metric = MetricIndicatorInline(\"48.7M\", \"tourists\", { direction: \"up\", value: 2.5 })\nov2 = OverviewCardItem(\"museums-monuments\", ov2top, ov2metric)\nov2top = IconText(ov2icon, \"neutral\", \"m\", \"Museums & Monuments\", \"Cultural sites\", false, \"vertical\")\nov2icon = Icon(\"landmark\", \"buildings\")\nov2metric = MetricIndicatorInline(\"2,370+\", \"across the city\")\nov3 = OverviewCardItem(\"tourism-revenue\", ov3top, ov3metric)\nov3top = IconText(ov3icon, \"neutral\", \"m\", \"Tourism Revenue\", \"2024 estimate\", false, \"vertical\")\nov3icon = Icon(\"circle-dollar-sign\", \"finance\")\nov3metric = MetricIndicatorInline(\"€23.4B\", \"generated\")\nsectionsBlock = SectionBlock([secLandmarks, secFood, secWhen], false)\nsecLandmarks = SectionItem(\"landmarks\", \"Iconic Landmarks\", [landmarksCarousel])\nlandmarksCarousel = VisualCardBlock([lm1, lm2, lm3, lm4], \"carousel\", true, { type: \"continue_conversation\", context: \"Tell me more about this Paris landmark\" })\nlm1 = VisualCardItem(lm1body, \"eiffel-tower\", \"https://c8.alamy.com/comp/RYEB13/aerial-view-of-the-eiffel-tower-with-the-park-champ-de-mars-and-the-river-seine-paris-france-RYEB13.jpg\", lm1tag, \"Aerial view of the Eiffel Tower\")\nlm1body = BoldText(\"text\", \"Eiffel Tower\", \"Open daily 9am–midnight\")\nlm1tag = Tag(\"Must-See\", lm1tagIcon, \"sm\", \"info\")\nlm1tagIcon = Icon(\"star\", \"shapes\")\nlm2 = VisualCardItem(lm2body, \"the-louvre\", \"https://images.fineartamerica.com/images/artworkimages/mediumlarge/3/1-exterior-of-musee-du-louvre-museum-paris-france-bernard-jaubert.jpg\", lm2tag, \"Exterior of the Louvre museum\")\nlm2body = BoldText(\"text\", \"The Louvre\", \"World's largest art museum\")\nlm2tag = Tag(\"8.9M visits/yr\", lm2tagIcon, \"sm\", \"success\")\nlm2tagIcon = Icon(\"trending-up\", \"charts\")\nlm3 = VisualCardItem(lm3body, \"notre-dame\", \"https://cdn.britannica.com/29/255529-050-63A22A3C/notre-dame-de-paris-cathedral-paris-france.jpg\", lm3tag, \"Notre-Dame cathedral facade\")\nlm3body = BoldText(\"text\", \"Notre-Dame\", \"Fully restored cathedral\")\nlm3tag = Tag(\"Reopened 2024\", lm3tagIcon, \"sm\", \"warning\")\nlm3tagIcon = Icon(\"hammer\", \"tools\")\nlm4 = VisualCardItem(lm4body, \"versailles\", \"https://cdn.britannica.com/76/130076-050-31128A3D/Latona-Fountain-Balthazard-Marsy-Gaspard-Andre-Le.jpg\", lm4tag, \"Latona Fountain at Versailles\")\nlm4body = BoldText(\"text\", \"Palace of Versailles\", \"Royal gardens & grand halls\")\nlm4tag = Tag(\"Day Trip\", lm4tagIcon, \"sm\", \"neutral\")\nlm4tagIcon = Icon(\"train-front\", \"travel\")\nsecFood = SectionItem(\"food\", \"Food & Drink\", [foodCards])\nfoodCards = ContextCardBlock([f1, f2, f3], \"grid\", true, { type: \"continue_conversation\", context: \"Tell me more about eating and drinking in Paris\" })\nf1 = ContextCardItem(\"bistros\", \"Bistros\", \"Classic French fare — try Les Arlots for confit beef cheeks.\", \"gray\", \"https://everydayparisian.com/wp-content/uploads/2023/01/IMG_0221-768x1024.webp\")\nf2 = ContextCardItem(\"fine-dining\", \"Fine Dining\", \"Septime leads a new wave of creative Michelin-starred cuisine.\", \"gray\", \"https://production-data.worldofmouth.app/images/72b2e6db-97ea-49c4-aa5d-3b7c8121c812.jpg\")\nf3 = ContextCardItem(\"bakeries\", \"Bakeries\", \"Shinya Pain for sourdough. Grand Prix-winning baguettes citywide.\", \"gray\", \"https://www.davidlebovitz.com/wp-content/uploads/2018/09/Le-petit-grain-paris-bakery-boulangerie-patisserie-pastry-shop-8-640x895.jpg\")\nsecWhen = SectionItem(\"when\", \"Best Time to Visit\", [whenTabs])\nwhenTabs = Tabs([tabSpring, tabSummer])\ntabSpring = TabItem(\"spring\", \"Spring\", [springList])\nspringList = ListBlock([sp1, sp2, sp3], \"number\")\nsp1 = ListItem(\"Cherry blossoms at Parc de Sceaux\", \"March – May\", null, null, { type: \"continue_conversation\", context: \"Tell me about cherry blossom season in Paris\" })\nsp2 = ListItem(\"Picnics along the Seine & in city parks\", \"Warm, lively atmosphere\", null, null, { type: \"continue_conversation\", context: \"Where are the best picnic spots in Paris?\" })\nsp3 = ListItem(\"Fewer crowds than summer\", \"Ideal for sightseeing\", null, null, { type: \"continue_conversation\", context: \"How busy is Paris in spring?\" })\ntabSummer = TabItem(\"summer\", \"Summer\", [summerList])\nsummerList = ListBlock([su1, su2, su3], \"number\")\nsu1 = ListItem(\"Swim in the Seine — open July & August 2026\", \"First time in 102 years\", null, null, { type: \"continue_conversation\", context: \"Tell me about swimming in the Seine\" })\nsu2 = ListItem(\"Open-air cinema at Parc de la Villette\", \"Free films under the stars\", null, null, { type: \"continue_conversation\", context: \"Tell me about the open-air cinema at Parc de la Villette\" })\nsu3 = ListItem(\"Bastille Day celebrations on July 14\", \"Fireworks at the Eiffel Tower\", null, null, { type: \"continue_conversation\", context: \"What happens in Paris on Bastille Day?\" })\nactionBtns = Buttons([btn1, btn2, btn3], \"row\")\nbtn1 = Button(\"Plan a Trip to Paris\", { type: \"continue_conversation\", context: \"Help me plan a trip to Paris\" }, \"primary\")\nbtn2 = Button(\"Best Hotels in Paris\", { type: \"continue_conversation\", context: \"Show me the best hotels in Paris\" }, \"secondary\")\nbtn3 = Button(\"Build a Paris Itinerary\", { type: \"continue_conversation\", context: \"Build a 5-day Paris itinerary for me\" }, \"secondary\")\nsrc1 = { title: \"Paris Travel Guide 2025\", sourceName: \"Travel and Tour World\", url: \"https://www.travelandtourworld.com/news/article/from-iconic-landmarks-to-hidden-gems-the-ultimate-paris-travel-guide-for-2025-you-need-to-read-now/\" }\nsrc2 = { title: \"Best Things to Do in Paris 2026\", sourceName: \"Time Out Paris\", url: \"http://www.timeout.fr/paris/en/for-tourists\" }\n\nExample 2 — Data and analytics response (quantitative asks lead with KPIs and charts):\n\nroot = Card([pageHeader, kpiBlock, sections, deepDiveHeader, compositeCards, actionButtons])\npageHeader = CardHeader(\"Analytics Dashboard\", \"April 2026 · Business Performance Overview\")\nkpiBlock = SnippetCardBlock([kpi1, kpi2, kpi3, kpi4], \"grid\", true)\nkpi1 = SnippetCardItem(\"total-revenue\", kpi1lhs, kpi1rhs)\nkpi1lhs = IconText(kpi1icon, \"neutral\", \"m\", \"Total Revenue\", \"Month to date\", false, \"horizontal\")\nkpi1icon = Icon(\"circle-dollar-sign\", \"finance\")\nkpi1rhs = BoldText(\"number\", \"$284,500\", \"+12.4%\", \"metric\")\nkpi2 = SnippetCardItem(\"active-users\", kpi2lhs, kpi2rhs)\nkpi2lhs = IconText(kpi2icon, \"neutral\", \"m\", \"Active Users\", \"Monthly active users\", false, \"horizontal\")\nkpi2icon = Icon(\"users\", \"people\")\nkpi2rhs = BoldText(\"number\", \"47,320\", \"+6.8%\", \"metric\")\nkpi3 = SnippetCardItem(\"conversion-rate\", kpi3lhs, kpi3rhs)\nkpi3lhs = IconText(kpi3icon, \"neutral\", \"m\", \"Conversion Rate\", \"Avg. this month\", false, \"horizontal\")\nkpi3icon = Icon(\"trending-up\", \"charts\")\nkpi3rhs = BoldText(\"number\", \"5.7%\", \"+0.9%\", \"metric\")\nkpi4 = SnippetCardItem(\"churn-rate\", kpi4lhs, kpi4rhs)\nkpi4lhs = IconText(kpi4icon, \"neutral\", \"m\", \"Churn Rate\", \"Monthly customer churn\", false, \"horizontal\")\nkpi4icon = Icon(\"user-minus\", \"people\")\nkpi4rhs = BoldText(\"number\", \"2.1%\", \"-0.4%\", \"metric\")\nsections = SectionBlock([secUsers, secChannels], false)\nsecUsers = SectionItem(\"users\", \"User Activity\", [userTabs])\nuserTabs = Tabs([tabGrowth, tabRetention])\ntabGrowth = TabItem(\"growth\", \"User Growth\", [growthChart])\ngrowthChart = LineChart(months, [dauSeries, mauSeries], \"natural\", \"Month\", \"Users (k)\")\nmonths = [\"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\", \"Jan\", \"Feb\", \"Mar\", \"Apr\"]\ndauSeries = Series(\"DAU\", [5.1, 5.4, 5.8, 6.2, 6.7, 7.1, 7.6, 8.0, 8.4, 8.9, 9.3, 9.8])\nmauSeries = Series(\"MAU\", [28.4, 30.1, 31.8, 33.5, 35.2, 37.1, 38.8, 40.4, 42.1, 43.9, 45.6, 47.3])\ntabRetention = TabItem(\"retention\", \"Cohort Retention\", [retentionChart])\nretentionChart = LineChart(cohortMonths, [cohortJan, cohortFeb, cohortMar], \"natural\", \"Month Since Signup\", \"Retention (%)\")\ncohortMonths = [\"Month 0\", \"Month 1\", \"Month 2\", \"Month 3\", \"Month 4\", \"Month 5\", \"Month 6\"]\ncohortJan = Series(\"Jan Cohort\", [100, 80, 70, 62, 56, 51, 47])\ncohortFeb = Series(\"Feb Cohort\", [100, 83, 73, 65, 59, 54, 50])\ncohortMar = Series(\"Mar Cohort\", [100, 86, 76, 68, 62, 0, 0])\nsecChannels = SectionItem(\"channels\", \"Acquisition Channels\", [channelTabs])\nchannelTabs = Tabs([tabChannelBar, tabChannelTable])\ntabChannelBar = TabItem(\"channel-bar\", \"Revenue by Channel\", [channelBarChart])\nchannelBarChart = HorizontalBarChart(channels, [channelSeries], \"grouped\", \"Revenue ($k)\", \"Channel\")\nchannels = [\"Organic Search\", \"Paid Ads\", \"Referral Program\", \"Partner Network\", \"Direct / Brand\", \"Email Campaigns\"]\nchannelSeries = Series(\"Revenue ($k)\", [82.4, 68.1, 54.6, 38.2, 26.8, 14.4])\ntabChannelTable = TabItem(\"channel-table\", \"Full Breakdown\", [channelTable])\nchannelTable = Table([colChannel, colLeads, colConv, colCac, colRevenue, colStatus])\ncolChannel = Col(\"Channel\", channels, \"string\")\ncolLeads = Col(\"Leads\", [4210, 3180, 2050, 1120, 1290, 820], \"number\")\ncolConv = Col(\"Conv. Rate\", [\"6.4%\", \"5.1%\", \"9.4%\", \"11.8%\", \"5.8%\", \"8.1%\"], \"string\")\ncolCac = Col(\"CAC ($)\", [188, 395, 132, 109, 218, 154], \"number\")\ncolRevenue = Col(\"Revenue ($k)\", [82.4, 68.1, 54.6, 38.2, 26.8, 14.4], \"number\")\ncolStatus = Col(\"Status\", [\"Scaling\", \"Optimizing\", \"Scaling\", \"Growing\", \"Stable\", \"Stable\"], \"string\")\ndeepDiveHeader = InlineHeader(\"Support Health & Marketing ROI\", \"Click a card to explore deeper insights\")\ncompositeCards = CompositeCardBlock([cardSupport, cardMarketing], \"grid\", true, { type: \"continue_conversation\", context: \"Explore this deep-dive area in more detail\" })\ncardSupport = CompositeCardItem(\"support-health\", supportHeader, [supportMetric, supportChart, supportList])\nsupportHeader = IconText(supportIcon, \"neutral\", \"m\", \"Support Health\", \"Ticket volume & resolution trends\", false, \"horizontal\")\nsupportIcon = Icon(\"headphones\", \"communication\")\nsupportMetric = MetricIndicatorInline(\"94.2% CSAT\", \"Customer Satisfaction Score\", { direction: \"up\", value: 2.1 })\nsupportChart = BarChart([\"Jan\", \"Feb\", \"Mar\", \"Apr\"], [openSeries, resolvedSeries], \"grouped\", \"Month\", \"Tickets\")\nopenSeries = Series(\"Opened\", [341, 327, 308, 289])\nresolvedSeries = Series(\"Resolved\", [319, 318, 301, 283])\nsupportList = EntityList([sup1, sup2, sup3], \"small\")\nsup1 = { left: \"Avg. Resolution Time\", right: \"4h 12m\", rightVariant: \"text\" }\nsup2 = { left: \"First Contact Resolution\", right: \"78.4%\", rightVariant: \"number\" }\nsup3 = { left: \"Escalation Rate\", right: \"6.2%\", rightVariant: \"number\" }\ncardMarketing = CompositeCardItem(\"marketing-roi\", marketingHeader, [marketingMetric, marketingChart, marketingList])\nmarketingHeader = IconText(marketingIcon, \"neutral\", \"m\", \"Marketing ROI\", \"Spend vs. pipeline generated\", false, \"horizontal\")\nmarketingIcon = Icon(\"megaphone\", \"communication\")\nmarketingMetric = MetricIndicatorInline(\"3.8x Blended ROI\", \"Return on Ad Spend\", { direction: \"up\", value: 0.4 })\nmarketingChart = AreaChart([\"Jan\", \"Feb\", \"Mar\", \"Apr\"], [spendSeries, pipelineSeries], \"natural\", \"Month\", \"Value ($k)\")\nspendSeries = Series(\"Ad Spend\", [44.0, 46.8, 49.2, 51.6])\npipelineSeries = Series(\"Pipeline Generated\", [158.2, 171.5, 184.9, 196.1])\nmarketingList = EntityList([mkt1, mkt2, mkt3], \"small\")\nmkt1 = { left: \"Total Ad Spend (Apr)\", right: \"$51,600\", rightVariant: \"number\" }\nmkt2 = { left: \"Cost per Lead\", right: \"$48.20\", rightVariant: \"number\" }\nmkt3 = { left: \"Top Channel\", right: \"Organic Search\", rightVariant: \"text\" }\nactionButtons = Buttons([btnChannels, btnForecast], \"row\")\nbtnChannels = Button(\"Break Down by Channel\", { type: \"continue_conversation\", context: \"Break down this month's revenue by acquisition channel\" }, \"primary\")\nbtnForecast = Button(\"Forecast Next Quarter\", { type: \"continue_conversation\", context: \"Forecast revenue and active users for next quarter\" }, \"secondary\")\n\nExample 3 — Form response (help-me-choose/plan asks: inspiration cards FIRST, then a compact form). The inspiration-card images below came from an image tool call — include such cards ONLY when you have real URLs; with no real URLs, start directly with the form:\n\nroot = Card([header, intro, inspoCards, form])\nheader = CardHeader(\"Plan Your Trip\", \"A few ideas to spark inspiration — then tell me your preferences\")\nintro = TextContent(\"Tap a style to jump straight in, or fill the quick form below for a tailored plan.\")\ninspoCards = VisualCardBlock([insp1, insp2], \"grid\", true, { type: \"continue_conversation\", context: \"Plan me a trip in this style with destinations, itinerary, and costs\" })\ninsp1 = VisualCardItem(insp1body, \"inspo-beach\", \"https://www.travelandleisure.com/thmb/cuAB7XMQ6s_Gji-no956KNopT7M=/1500x0/filters:no_upscale():max_bytes(150000):strip_icc()/TAL-maldives-TROPVACAY0325-1b5047d5931d4850b47d082966f9f563.jpg\", insp1tag, \"Maldives overwater villas\")\ninsp1body = BoldText(\"text\", \"Tropical Beach Escape\", \"Overwater villas & snorkeling\")\ninsp1tag = Tag(\"Relaxation\", insp1icon, \"sm\", \"info\")\ninsp1icon = Icon(\"umbrella\", \"travel\")\ninsp2 = VisualCardItem(insp2body, \"inspo-mountain\", \"https://www.salkantaytrekmachu.com/img/adventure-travel-peru-032.jpg\", insp2tag, \"Trekking in the Peruvian Andes\")\ninsp2body = BoldText(\"text\", \"Mountain Adventure\", \"Trekking & epic views\")\ninsp2tag = Tag(\"Active\", insp2icon, \"sm\", \"warning\")\ninsp2icon = Icon(\"mountain\", \"nature\")\nform = Form(\"trip-planner\", formButtons, [fcDestination, fcDates, fcTravellers, fcTripType, fcBudget, fcStay, fcNotes])\nformButtons = Buttons([btnSubmit, btnSkip], \"row\")\nbtnSubmit = Button(\"Plan My Trip\", { type: \"continue_conversation\", context: \"Help me plan a trip based on my form submission\" }, \"primary\")\nbtnSkip = Button(\"Just Surprise Me\", { type: \"continue_conversation\", context: \"Skip the form and surprise me with a destination and itinerary\" }, \"secondary\")\nfcDestination = FormControl(\"Destination\", destinationInput, \"Leave blank if you'd like suggestions\")\ndestinationInput = Input(\"destination\", \"e.g. Tokyo, Japan — or leave blank\", \"text\")\nfcDates = FormControl(\"Travel Dates\", datesPicker, \"Select your start and end dates\")\ndatesPicker = DatePicker(\"travel-dates\", \"range\", { required: true })\nfcTravellers = FormControl(\"Number of Travellers\", travellersSelect, \"How many people are travelling?\")\ntravellersSelect = Select(\"num-travellers\", [tr1, tr2, tr3, tr4], \"Select number of travellers\", { required: true })\ntr1 = SelectItem(\"1\", \"1 — Solo\")\ntr2 = SelectItem(\"2\", \"2 — Couple\")\ntr3 = SelectItem(\"3-4\", \"3–4 People\")\ntr4 = SelectItem(\"5+\", \"5+ People\")\nfcTripType = FormControl(\"Trip Type\", tripTypeCards, \"What kind of trip are you planning?\")\ntripTypeCards = OptionCards(\"trip-type\", \"single\", [oc1, oc2, oc3, oc4], { required: true })\noc1 = OptionCard(\"adventure\", \"Adventure\", \"Hiking, trekking & outdoor activities\", oc1icon)\noc1icon = Icon(\"mountain\", \"nature\")\noc2 = OptionCard(\"beach\", \"Beach & Relaxation\", \"Sun, sand & leisure\", oc2icon)\noc2icon = Icon(\"waves\", \"nature\")\noc3 = OptionCard(\"cultural\", \"Cultural & Historical\", \"Museums, heritage & local experiences\", oc3icon)\noc3icon = Icon(\"landmark\", \"buildings\")\noc4 = OptionCard(\"city\", \"City Break\", \"Urban exploration & nightlife\", oc4icon)\noc4icon = Icon(\"building-2\", \"buildings\")\nfcBudget = FormControl(\"Estimated Budget (per person)\", budgetSlider, \"Drag to set your per-person budget in USD\")\nbudgetSlider = Slider(\"budget\", \"discrete\", 500, 10000, 500, [2500], \"Budget (USD)\")\nfcStay = FormControl(\"Accommodation Preference\", stayChips, \"Select all that apply\")\nstayChips = Chips(\"accommodation\", \"multiple\", [ch1, ch2, ch3, ch4], {}, [\"hotel\"])\nch1 = ChipItem(\"hotel\", \"Hotel\", ch1icon)\nch1icon = Icon(\"building\", \"buildings\")\nch2 = ChipItem(\"resort\", \"Resort\", ch2icon)\nch2icon = Icon(\"umbrella\", \"travel\")\nch3 = ChipItem(\"airbnb\", \"Vacation Rental\", ch3icon)\nch3icon = Icon(\"home\", \"home\")\nch4 = ChipItem(\"camping\", \"Camping / Glamping\", ch4icon)\nch4icon = Icon(\"tent\", \"nature\")\nfcNotes = FormControl(\"Special Requests or Notes\", notesArea, \"Any dietary needs, accessibility requirements, or specific interests?\")\nnotesArea = TextArea(\"special-requests\", \"e.g. vegetarian meals, wheelchair access, honeymoon surprise...\", 4)\n\nExample 4 — Recommendations with images and links (same pattern for products, movies, restaurants, articles):\n\nroot = Card([header, intro, posters, linksHeader, trackCards], [src1, src2])\nheader = CardHeader(\"Retro-Energetic Indian Tracks\", \"New Hindi releases with 80s/90s dance DNA\")\nintro = TextContent(\"These recent tracks channel retro synths and 90s party percussion [1][2]. Tap a poster to explore a track, or use its button to watch the exact official video.\")\nposters = VisualCardBlock([v1, v2], \"grid\", true, { type: \"continue_conversation\", context: \"Tell me more about this track and its retro influences\" })\nv1 = VisualCardItem(v1body, \"tamma\", \"https://i.ytimg.com/vi/PQHeOb0Q9oo/hq720.jpg\", v1tag, \"Tamma Tamma video poster\")\nv1body = BoldText(\"text\", \"Tamma Tamma\", \"Dhurandhar · Ranveer Singh\")\nv1tag = Tag(\"80s Electro-Disco\", v1icon, \"sm\", \"success\")\nv1icon = Icon(\"disc-3\", \"multimedia\")\nv2 = VisualCardItem(v2body, \"sajan\", \"https://i.ytimg.com/vi/mLz-qBiRnl8/hq720.jpg\", v2tag, \"Sajan Re video poster\")\nv2body = BoldText(\"text\", \"Sajan Re\", \"Nora Fatehi · Badshah\")\nv2tag = Tag(\"Synthwave Electro\", v2icon, \"sm\", \"info\")\nv2icon = Icon(\"music\", \"multimedia\")\nlinksHeader = InlineHeader(\"Watch the Exact Videos\", \"Each button opens that track's official video\")\ntrackCards = CompositeCardBlock([t1, t2], \"grid\", true)\nt1 = CompositeCardItem(\"t-tamma\", t1h, [t1body, t1tags], { button: t1btn })\nt1h = IconText(t1icon, \"neutral\", \"m\", \"Tamma Tamma\", \"Bappi Lahiri classic, electro-funk rework\", true, \"horizontal\")\nt1icon = Icon(\"disc-3\", \"multimedia\")\nt1body = Text(\"text\", \"Heavy analog synths and brass over a fast disco groove.\", \"Retro Electro-Disco\")\nt1tags = TagBlock([\"2026 Film Hit\", \"Peak Party\"])\nt1btn = Button(\"Watch on YouTube\", { type: \"open_url\", url: \"https://www.youtube.com/watch?v=PQHeOb0Q9oo\" }, \"primary\")\nt2 = CompositeCardItem(\"t-sajan\", t2h, [t2body, t2tags], { button: t2btn })\nt2h = IconText(t2icon, \"neutral\", \"m\", \"Sajan Re\", \"Nora Fatehi · Badshah · Sanjoy\", true, \"horizontal\")\nt2icon = Icon(\"music\", \"multimedia\")\nt2body = Text(\"text\", \"Synthwave melody hooks over a modern club beat.\", \"Retro-Fusion Electro\")\nt2tags = TagBlock([\"Club Anthem\", \"Nora x Badshah\"])\nt2btn = Button(\"Watch on YouTube\", { type: \"open_url\", url: \"https://www.youtube.com/watch?v=COUSCnFazzc\" }, \"primary\")\nsrc1 = { title: \"Tamma Tamma (Full Video) - Dhurandhar\", sourceName: \"YouTube (T-Series)\", url: \"https://www.youtube.com/watch?v=PQHeOb0Q9oo\" }\nsrc2 = { title: \"Sajan Re (Official Music Video)\", sourceName: \"YouTube (T-Series)\", url: \"https://www.youtube.com/watch?v=COUSCnFazzc\" }\n\nExample 5 — Table with follow-ups:\n\nroot = Card([title, tbl, followUps])\ntitle = TextContent(\"Top Languages\", \"large-heavy\")\ntbl = Table([Col(\"Language\", langs), Col(\"Users (M)\", users), Col(\"Year\", years)])\nlangs = [\"Python\", \"JavaScript\", \"Java\"]\nusers = [15.7, 14.2, 12.1]\nyears = [1991, 1995, 1995]\nfollowUps = FollowUpBlock([fu1, fu2])\nfu1 = FollowUpItem(\"Tell me more about Python\")\nfu2 = FollowUpItem(\"Show me a JavaScript comparison\")\n\n## Important Rules\n- Choose components that best represent the content (tables for comparisons, charts for trends, forms for input, etc.)\n\n## Final Verification\nBefore finishing, walk your output and verify:\n1. root = Card(...) is the FIRST line (for optimal streaming).\n2. Every referenced name is defined. Every defined name (other than root) is reachable from root.\n\n- When asked about data, generate realistic/plausible data\n- Every response is a single Card(children) — children stack vertically automatically. No layout params are needed on Card.\n- Card is the only layout container. Do NOT use Stack. Use Tabs to switch between sections, Carousel for horizontal scroll.\n- Use FollowUpBlock at the END of a Card to suggest what the user can do or ask next.\n- Use ListBlock when presenting a set of options or steps; give each ListItem an action (5th argument) to make it clickable — without one the item is plain text.\n- Use SectionBlock to group long responses into collapsible sections — good for reports, FAQs, and structured content.\n- Use SectionItem inside SectionBlock: each item needs a unique value id, a trigger (header label), and a content array.\n- Carousel takes an array of slides, where each slide is an array of content: carousel = Carousel([[t1, img1], [t2, img2]])\n- IMPORTANT: Every slide in a Carousel must use the same component structure in the same order — e.g. all slides: [title, image, description, tags].\n- For forms, define one FormControl reference per field so controls can stream progressively.\n- For forms, always provide the second Form argument with Buttons(...) actions: Form(name, buttons, fields).\n- Never nest Form inside Form.\n- CardHeader is the page-level heading — place it as the first child of Card; InlineHeader is a lighter subsection label for a block inside the response, and is redundant when the following component already has a heading.\n- Never use Callout or TextCallout to justify why you did what you did or for any meta-information.\n- Every image slot (Image, ImageBlock, ImageGallery, VisualCardItem/ContextCardItem bgImageSrc, ImageText, ImageTextLarge, ListItem image) requires a REAL URL from a tool result, the conversation, or the user — NEVER invent, guess, or template a URL.\n- If no image URL is in context, compose without image components — icons, gray ContextCardItems, and charts still make rich layouts.\n- Argument-order trap: Image takes alt FIRST, while ImageBlock, ImageText, and ImageTextLarge take src FIRST — alt text in a src slot ships a broken image.\n- Card blocks (SnippetCardBlock, OverviewCardBlock, ContextCardBlock, CompositeCardBlock, VisualCardBlock) need at least 2 items, all with the same structure; use layout \"grid\" for primary side-by-side comparison and \"carousel\" for secondary browsing.\n- The card-block action is BLOCK-LEVEL: one action shared by every card, with the clicked card's details attached automatically — keep continue_conversation context generic (e.g. \"Tell me more about this destination\"), and never use a block-level open_url since it would send every card to the same URL.\n- Card items have NO per-item action: to give each item its own external link, use CompositeCardItem with footer { button: Button(label, { type: \"open_url\", url }) } — never claim a card opens a link it cannot.\n- Text, BoldText, IconText, ImageText, ImageTextLarge and MetricIndicator* are inline building blocks used INSIDE card items — do not place them directly in the root Card.\n- EntityList size \"small\" is reserved for CompositeCardItem body content (no header/footer); use size \"default\" elsewhere.\n- Tag variant must be semantically correct (success=positive, danger=negative, warning=cautionary, info=informational, neutral=label); Tag takes an optional Icon second, so use Tag(text, icon, size, variant) when a variant is needed.\n- TagBlock takes plain strings — keep it to at most 3 tags.\n- ListBlock variant \"number\" is for ordered sequences and rankings, \"image\" for products/profiles with a real image; Steps is for ordered sequential processes only.\n- Use SectionBlock to divide a long response into independent, equally prominent topics; use Tabs when the items are alternative views of the same thing; set isFoldable=false unless a section has 4+ distinct blocks; never make SectionBlock the first Card child.\n- Accordion is for supplementary or FAQ content only — never primary information, 3-5 items max, and always the last component in its section or card.\n- Charts: LineChart/AreaChart variant \"natural\" for smooth trends and \"linear\" for precise point-to-point data; BarChart \"grouped\" for side-by-side comparison and \"stacked\" for part-to-whole; prefer HorizontalBarChart when category labels are long or numerous.\n- For large chart values, scale the data and put the unit in the axis label (e.g. yLabel \"Revenue ($k)\") instead of passing raw thousands or millions.\n- Quantitative asks get a chart (plus KPI cards where useful) in the FIRST response — do not answer numbers with prose or a table alone.\n- Table is column-oriented: each Col holds its own data array of plain strings or numbers (never components), and every column's array must have the same length.\n- Use EditableTable ONLY when the user or app explicitly asks for an editable table; default to Table for read-only data.\n- Every Form MUST have EXACTLY ONE submit Button with variant \"primary\" — that button validates the form; other buttons (secondary/tertiary escape hatches like \"Just Surprise Me\") skip validation, and there is never a reset button.\n- Prefer structured inputs (Select, Chips, OptionCards, DatePicker, Slider) over free-text Input, give every FormControl a clear label (and a hint where it prevents mistakes), and keep each field name unique within the response.\n- Validation rules ({ required: true, email: true, minLength: 8, ... }) are enforced with inline errors — use them for genuinely essential fields, but never require a free-text field where leaving it blank is meaningful.\n- Form inputs work ONLY inside a FormControl within a Form; for tappable suggestion choices OUTSIDE a form, use Buttons of continue_conversation Buttons — never a bare OptionCards or Chips.\n- Only add Buttons for real actions: form submit, open_url with a URL from a tool or the user (never fabricated), or a continue_conversation follow-up — never a button for an app action (open, download, export, edit) you cannot perform.\n- Every Button label must accurately describe what clicking it does; use \"primary\" for the main action and \"secondary\"/\"tertiary\" only for lower-emphasis supporting actions.\n- Icon: always include the category argument — it enables fallback matching when the exact lucide name is unavailable.\n- Pass sources on Card ONLY when the answer relies on real references you actually have, and cite them inline in TextContent as [1], [2] (1-based index into sources)."}, +{"name":"openui-default","spec":{"root":"Stack","components":{"Card":{"signature":"Card(children: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock | Tabs | Carousel | Stack)[], variant?: \"card\" | \"sunk\" | \"clear\", direction?: \"row\" | \"column\", gap?: \"none\" | \"xs\" | \"s\" | \"m\" | \"l\" | \"xl\" | \"2xl\", align?: \"start\" | \"center\" | \"end\" | \"stretch\" | \"baseline\", justify?: \"start\" | \"center\" | \"end\" | \"between\" | \"around\" | \"evenly\", wrap?: boolean)","description":"Styled container. variant: \"card\" (default, elevated) | \"sunk\" (recessed) | \"clear\" (transparent). Always full width. Accepts all Stack flex params (default: direction \"column\"). Cards flex to share space in row/wrap layouts."},"CardHeader":{"signature":"CardHeader(title?: string, subtitle?: string)","description":"Header with optional title and subtitle"},"TextContent":{"signature":"TextContent(text: string, size?: \"small\" | \"default\" | \"large\" | \"small-heavy\" | \"large-heavy\")","description":"Text block. Supports markdown. Optional size: \"small\" | \"default\" | \"large\" | \"small-heavy\" | \"large-heavy\"."},"MarkDownRenderer":{"signature":"MarkDownRenderer(textMarkdown: string, variant?: \"clear\" | \"card\" | \"sunk\")","description":"Renders markdown text with optional container variant"},"Callout":{"signature":"Callout(variant: \"info\" | \"warning\" | \"error\" | \"success\" | \"neutral\", title: string, description: string, visible?: $binding)","description":"Callout banner. Optional visible is a reactive $boolean — auto-dismisses after 3s by setting $visible to false."},"TextCallout":{"signature":"TextCallout(variant?: \"neutral\" | \"info\" | \"warning\" | \"success\" | \"danger\", title?: string, description?: string)","description":"Text callout with variant, title, and description"},"Image":{"signature":"Image(alt: string, src?: string)","description":"Image with alt text and optional URL"},"ImageBlock":{"signature":"ImageBlock(src: string, alt?: string)","description":"Image block with loading state"},"ImageGallery":{"signature":"ImageGallery(images: {src: string, alt?: string, details?: string}[])","description":"Gallery grid of images with modal preview"},"CodeBlock":{"signature":"CodeBlock(language: string, codeString: string)","description":"Syntax-highlighted code block"},"InlineHeader":{"signature":"InlineHeader(heading: string, description?: string)","description":"Compact section heading with an optional one-line description, for use inside cards."},"Table":{"signature":"Table(columns: Col[])","description":"Data table — column-oriented. Each Col holds its own data array."},"Col":{"signature":"Col(label: string, data: any, type?: \"string\" | \"number\" | \"action\")","description":"Column definition — holds label + data array"},"EditableTable":{"signature":"EditableTable(name?: string, columns?: {type: \"text\" | \"number\" | \"date-single\" | \"select\" | \"url\", key?: string, header?: string, width?: number, options?: {value: string, label: string}[]}[], data?: {id: string, values: (string | number)[]}[])","description":"Spreadsheet-like table whose cells the user can edit inline (text, number, url, date, select columns); edits are saved back as a form field"},"BarChart":{"signature":"BarChart(labels: string[], series: Series[], variant?: \"grouped\" | \"stacked\", xLabel?: string, yLabel?: string, height?: number)","description":"Vertical bars; use for comparing values across categories with one or more series"},"LineChart":{"signature":"LineChart(labels: string[], series: Series[], variant?: \"linear\" | \"natural\" | \"step\", xLabel?: string, yLabel?: string, height?: number)","description":"Lines over categories; use for trends and continuous data over time"},"AreaChart":{"signature":"AreaChart(labels: string[], series: Series[], variant?: \"linear\" | \"natural\" | \"step\", xLabel?: string, yLabel?: string, height?: number)","description":"Filled area under lines; use for cumulative totals or volume trends over time"},"RadarChart":{"signature":"RadarChart(labels: string[], series: Series[])","description":"Spider/web chart; use for comparing multiple variables across one or more entities"},"HorizontalBarChart":{"signature":"HorizontalBarChart(labels: string[], series: Series[], variant?: \"grouped\" | \"stacked\", xLabel?: string, yLabel?: string)","description":"Horizontal bars; prefer when category labels are long or for ranked lists"},"Series":{"signature":"Series(category: string, values: number[])","description":"One data series"},"PieChart":{"signature":"PieChart(labels: string[], values: number[], variant?: \"pie\" | \"donut\", appearance?: \"circular\" | \"semiCircular\")","description":"Circular slices; use plucked arrays: PieChart(data.categories, data.values)"},"RadialChart":{"signature":"RadialChart(labels: string[], values: number[])","description":"Radial bars; use plucked arrays: RadialChart(data.categories, data.values)"},"SingleStackedBarChart":{"signature":"SingleStackedBarChart(labels: string[], values: number[])","description":"Single horizontal stacked bar; use plucked arrays: SingleStackedBarChart(data.categories, data.values)"},"Slice":{"signature":"Slice(category: string, value: number)","description":"One slice with label and numeric value"},"ScatterChart":{"signature":"ScatterChart(datasets: ScatterSeries[], xLabel?: string, yLabel?: string)","description":"X/Y scatter plot; use for correlations, distributions, and clustering"},"ScatterSeries":{"signature":"ScatterSeries(name: string, points: Point[])","description":"Named dataset"},"Point":{"signature":"Point(x: number, y: number, z?: number)","description":"Data point with numeric coordinates"},"Form":{"signature":"Form(name: string, buttons: Buttons, fields?: FormControl[])","description":"Form container with fields and explicit action buttons"},"FormControl":{"signature":"FormControl(label: string, input: Input | TextArea | Select | DatePicker | Slider | CheckBoxGroup | RadioGroup | Chips | OptionCards, hint?: string)","description":"Field with label, input component, and optional hint text"},"Label":{"signature":"Label(text: string)","description":"Text label"},"Input":{"signature":"Input(name: string, placeholder?: string, type?: \"text\" | \"email\" | \"password\" | \"number\" | \"url\", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)","description":""},"TextArea":{"signature":"TextArea(name: string, placeholder?: string, rows?: number, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)","description":""},"Select":{"signature":"Select(name: string, items: SelectItem[], placeholder?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding, size?: \"small\" | \"medium\" | \"large\")","description":""},"SelectItem":{"signature":"SelectItem(value: string, label: string)","description":"Option for Select"},"DatePicker":{"signature":"DatePicker(name: string, mode?: \"single\" | \"range\", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)","description":""},"Slider":{"signature":"Slider(name: string, variant: \"continuous\" | \"discrete\", min: number, max: number, step?: number, defaultValue?: number[], label?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)","description":"Numeric slider input; supports continuous and discrete (stepped) variants"},"CheckBoxGroup":{"signature":"CheckBoxGroup(name: string, items: CheckBoxItem[], rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding>)","description":""},"CheckBoxItem":{"signature":"CheckBoxItem(label: string, description: string, name: string, defaultChecked?: boolean)","description":""},"RadioGroup":{"signature":"RadioGroup(name: string, items: RadioItem[], defaultValue?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)","description":""},"RadioItem":{"signature":"RadioItem(label: string, description: string, value: string)","description":""},"SwitchGroup":{"signature":"SwitchGroup(name: string, items: SwitchItem[], variant?: \"clear\" | \"card\" | \"sunk\", value?: $binding>)","description":"Group of switch toggles"},"SwitchItem":{"signature":"SwitchItem(label?: string, description?: string, name: string, defaultChecked?: boolean)","description":"Individual switch toggle"},"ChipItem":{"signature":"ChipItem(value: string, label: string, icon?: Icon, disabled?: boolean)","description":"A single selectable chip inside a Chips group, with a value, label and optional icon."},"Chips":{"signature":"Chips(name: string, type?: \"single\" | \"multiple\", items?: ChipItem[], rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, defaultValue?: string | string[])","description":"A form field of compact selectable chips for choosing one or many short options; the selection is stored under `name`."},"OptionCard":{"signature":"OptionCard(value: string, title: string, subtitle?: string, topContent?: Icon | Image, disabled?: boolean)","description":"A single selectable card inside an OptionCards group, with a value, title, optional subtitle and an optional Icon or Image on top."},"OptionCards":{"signature":"OptionCards(name: string, type?: \"single\" | \"multiple\", items?: OptionCard[], rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, defaultValue?: string | string[])","description":"A form field of selectable cards (title, optional subtitle, optional icon or image) laid out in a responsive grid for choosing one or many options; the selection is stored under `name`."},"Button":{"signature":"Button(label: string, action?: ActionExpression, variant?: \"primary\" | \"secondary\" | \"tertiary\", type?: \"normal\" | \"destructive\", size?: \"extra-small\" | \"small\" | \"medium\" | \"large\")","description":"Clickable button"},"Buttons":{"signature":"Buttons(buttons: Button[], direction?: \"row\" | \"column\")","description":"Group of Button components. direction: \"row\" (default) | \"column\"."},"IconButton":{"signature":"IconButton(name: string, icon: Icon, action?: ActionExpression, variant?: \"primary\" | \"secondary\" | \"tertiary\", size?: \"extra-small\" | \"small\" | \"medium\" | \"large\", shape?: \"square\" | \"circle\")","description":"Icon-only button. name is the accessible label and the action label; icon is an Icon; action fires on click."},"Stack":{"signature":"Stack(children: any[], direction?: \"row\" | \"column\", gap?: \"none\" | \"xs\" | \"s\" | \"m\" | \"l\" | \"xl\" | \"2xl\", align?: \"start\" | \"center\" | \"end\" | \"stretch\" | \"baseline\", justify?: \"start\" | \"center\" | \"end\" | \"between\" | \"around\" | \"evenly\", wrap?: boolean)","description":"Flex container. direction: \"row\"|\"column\" (default \"column\"). gap: \"none\"|\"xs\"|\"s\"|\"m\"|\"l\"|\"xl\"|\"2xl\" (default \"m\"). align: \"start\"|\"center\"|\"end\"|\"stretch\"|\"baseline\". justify: \"start\"|\"center\"|\"end\"|\"between\"|\"around\"|\"evenly\"."},"Tabs":{"signature":"Tabs(items: TabItem[])","description":"Tabbed container"},"TabItem":{"signature":"TabItem(value: string, trigger: string, content: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock)[])","description":"value is unique id, trigger is tab label, content is array of components"},"Accordion":{"signature":"Accordion(items: AccordionItem[])","description":"Collapsible sections"},"AccordionItem":{"signature":"AccordionItem(value: string, trigger: string, content: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock)[])","description":"value is unique id, trigger is section title"},"Steps":{"signature":"Steps(items: StepsItem[])","description":"Step-by-step guide"},"StepsItem":{"signature":"StepsItem(title: string, details: string)","description":"title and details text for one step"},"Carousel":{"signature":"Carousel(children: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock)[][], variant?: \"card\" | \"sunk\")","description":"Horizontal scrollable carousel"},"Separator":{"signature":"Separator(orientation?: \"horizontal\" | \"vertical\", decorative?: boolean)","description":"Visual divider between content sections"},"TagBlock":{"signature":"TagBlock(tags: string[], size?: \"sm\" | \"md\" | \"lg\")","description":"tags is an array of strings; optional size sm | md | lg"},"Tag":{"signature":"Tag(text: string, icon?: Icon, size?: \"sm\" | \"md\" | \"lg\", variant?: \"neutral\" | \"info\" | \"success\" | \"warning\" | \"danger\")","description":"Styled tag/badge with optional Icon and variant"},"Icon":{"signature":"Icon(name: string, category?: string)","description":"A lucide icon by kebab-case name (e.g. 'circle-check'). Optional category picks a topical fallback when the name doesn't resolve."},"EntityList":{"signature":"EntityList(rows?: {left: string, right: string, rightVariant?: \"text\" | \"number\"}[], size?: \"small\" | \"default\", header?: {left: string, right: string, rightVariant?: \"text\" | \"number\"}, footer?: {left: string, right: string, rightVariant?: \"text\" | \"number\"})","description":"Two-column key/value rows (left label, right value). size 'default' supports optional header and footer rows; rightVariant 'number' uses tabular numbers."},"ListBlock":{"signature":"ListBlock(items: ListItem[], variant?: \"number\" | \"image\", size?: \"default\" | \"small\")","description":"A list of items with number or image indicators. Each item can optionally have an action. size small renders a compact list."},"ListItem":{"signature":"ListItem(title: string, subtitle?: string, image?: {src: string, alt: string}, actionLabel?: string, action?: ActionExpression)","description":"Item in a ListBlock — displays a title with an optional subtitle and image. When action is provided, the item becomes clickable."},"Text":{"signature":"Text(variant?: \"text\" | \"number\", value: string, subtext?: string, subtextVariant?: \"text\" | \"number\" | \"metric\", size?: \"xs\" | \"sm\" | \"md\" | \"lg\")","description":"Plain text line with optional subtext. variant 'number' uses tabular number styling; subtextVariant 'metric' colors a leading +/- subtext green/red."},"BoldText":{"signature":"BoldText(variant?: \"text\" | \"number\", value: string, subtext?: string, subtextVariant?: \"text\" | \"number\" | \"metric\", size?: \"xs\" | \"sm\" | \"md\" | \"lg\")","description":"Emphasized (bold) text line with optional subtext. variant 'number' uses tabular number styling; subtextVariant 'metric' colors a leading +/- subtext green/red."},"IconText":{"signature":"IconText(icon: Icon, iconVariant?: \"neutral\" | \"info\" | \"success\" | \"warning\" | \"danger\" | \"inverted\" | \"filled\" | \"soft\", iconSize?: \"xs\" | \"s\" | \"m\" | \"l\" | \"xl\" | \"sm\" | \"md\" | \"lg\", title: string, subtitle?: string, bold?: boolean, layout?: \"horizontal\" | \"vertical\")","description":"An icon badge with a title and optional subtitle, laid out horizontally or vertically. iconVariant sets the badge color."},"ImageText":{"signature":"ImageText(src: string, alt?: string, title: string, subtitle?: string, bold?: boolean, layout?: \"horizontal\" | \"vertical\", imageSize?: number)","description":"A small square image (thumbnail/avatar) with a title and optional subtitle. src must be a real image URL."},"ImageTextLarge":{"signature":"ImageTextLarge(src: string, alt?: string, title: string, subtitle?: string, bold?: boolean)","description":"A full-width banner image above a bold title and optional subtitle. src must be a real image URL."},"MetricIndicatorInline":{"signature":"MetricIndicatorInline(value: string, subtext?: string, trend?: {direction: \"up\" | \"down\", value: number})","description":"Headline metric value with an optional +/- percentage trend and subtext, all on one line."},"MetricIndicatorWithStrikethrough":{"signature":"MetricIndicatorWithStrikethrough(value: string, subtext?: string, previousValue?: string, trend?: {direction: \"up\" | \"down\", value: number})","description":"Headline metric value with an optional struck-through previousValue, a +/- percentage trend, and subtext below."},"SnippetCardItem":{"signature":"SnippetCardItem(id?: string, lhs: IconText | ImageText, rhs?: Text | BoldText)","description":"One row-style snippet card: a label on the left (IconText or ImageText) and an optional value on the right (Text or BoldText)."},"SnippetCardBlock":{"signature":"SnippetCardBlock(items: SnippetCardItem[], layout?: \"grid\", responsive?: boolean, action?: ActionExpression, gap?: number | string)","description":"A responsive grid of compact label/value cards (2 per row) for showing several short facts side by side; optionally clickable with a shared action."},"OverviewCardItem":{"signature":"OverviewCardItem(id?: string, top: IconText | ImageText | Text, bottom?: MetricIndicatorInline)","description":"One overview card: a heading slot at the top (IconText, ImageText or Text) and an optional MetricIndicatorInline at the bottom."},"OverviewCardBlock":{"signature":"OverviewCardBlock(items: OverviewCardItem[], layout?: \"grid\" | \"carousel\", responsive?: boolean, action?: ActionExpression, gap?: number | string)","description":"A grid or horizontal carousel of compact overview cards, each with a heading (icon/image/text) on top and an inline metric below; optionally clickable with a shared action."},"ContextCardItem":{"signature":"ContextCardItem(id?: string, title: string | Tag, body?: string, bgColor?: \"gray\", bgImageSrc?: string, bgImageAlt?: string)","description":"A single card inside a ContextCardBlock: a title (plain string or Tag), an optional markdown body, and an optional gray tint or background image."},"ContextCardBlock":{"signature":"ContextCardBlock(items: ContextCardItem[], layout?: \"grid\" | \"carousel\", responsive?: boolean, action?: ActionExpression, gap?: number | string)","description":"A grid or carousel of compact tinted context cards (title or tag plus a short bold body); an optional action makes every card clickable."},"CompositeCardItem":{"signature":"CompositeCardItem(id?: string, header?: IconText | ImageText | ImageTextLarge | Text | Image, body?: (Text | BoldText | MetricIndicatorInline | IconText | Image | AreaChart | BarChart | LineChart | ListBlock | TagBlock | EntityList)[], footer?: {price?: BoldText | MetricIndicatorWithStrikethrough, button?: Button})","description":"A single card inside a CompositeCardBlock: an optional header (icon/image/text), a stack of body elements (text, metrics, charts, lists, tags), and an optional price/button footer."},"CompositeCardBlock":{"signature":"CompositeCardBlock(items: CompositeCardItem[], layout?: \"grid\" | \"carousel\", responsive?: boolean, action?: ActionExpression, gap?: number | string)","description":"A two-per-row grid or carousel of rich cards, each with an optional header, stacked body content (text, metrics, charts, lists, tags) and a price/button footer; an optional action makes every card clickable."},"VisualCardItem":{"signature":"VisualCardItem(body: BoldText, id?: string, bgImageSrc?: string, tag?: Tag, bgImageAlt?: string)","description":"A single photo-first card inside a VisualCardBlock: a BoldText body panel, an optional Tag, and a background image (bgImageSrc must be a real URL; bgImageAlt is its alt text)."},"VisualCardBlock":{"signature":"VisualCardBlock(items: VisualCardItem[], layout?: \"grid\" | \"carousel\", responsive?: boolean, action?: ActionExpression, gap?: number | string)","description":"A grid or carousel of photo-first cards: a full-bleed background image with a tag on top and a bold text panel at the bottom; an optional action makes every card clickable."},"Modal":{"signature":"Modal(title: string, open?: $binding, children: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock)[], size?: \"sm\" | \"md\" | \"lg\")","description":"Modal dialog. open is a reactive $boolean binding — set to true to open, X/Escape/backdrop auto-closes. Put Form with buttons inside children."}},"componentGroups":[{"name":"Layout","components":["Stack","Tabs","TabItem","Accordion","AccordionItem","Steps","StepsItem","Carousel","Separator","Modal"],"notes":["- For grid-like layouts, use Stack with direction \"row\" and wrap set to true.","- Prefer justify \"start\" (or omit justify) with wrap=true for stable columns instead of uneven gutters.","- Use nested Stacks when you need explicit rows/sections.","- Show/hide sections: $editId != \"\" ? Card([editForm]) : null","- Modal: Modal(\"Title\", $showModal, [content]) — $showModal is boolean, X/Escape auto-closes. Put Form with its own buttons inside children.","- Use Tabs for alternative views (chart types, data sections) — no $variable needed","- Shared filter across Tabs: same $days binding in Query args works across all TabItems"]},{"name":"Content","components":["Card","CardHeader","TextContent","MarkDownRenderer","Callout","TextCallout","Image","ImageBlock","ImageGallery","CodeBlock","InlineHeader"],"notes":["- InlineHeader is a compact heading + description pair for labelling a block (lighter than CardHeader).","- Use Cards to group related KPIs or sections. Stack with direction \"row\" for side-by-side layouts.","- Success toast: Callout(\"success\", \"Saved\", \"Done.\", $showSuccess) — use @Set($showSuccess, true) in save action, auto-dismisses after 3s. For errors: result.status == \"error\" ? Callout(\"error\", \"Failed\", result.error) : null","- KPI card: Card([TextContent(\"Label\", \"small\"), TextContent(\"\" + @Count(@Filter(data.rows, \"field\", \"==\", \"value\")), \"large-heavy\")])"]},{"name":"Tables","components":["Table","Col","EditableTable"],"notes":["- EditableTable lets the user edit cells inline. Give it a unique name, columns of { type, key, header } with type one of text | number | date-single | select | url (select also needs options: [{ value, label }]).","- data is an array of { id, values } rows where values are ordered positionally to match columns. Edited data is submitted when the user clicks Save Changes.","- Table is COLUMN-oriented: Table([Col(\"Label\", dataArray), Col(\"Count\", countArray, \"number\")]). Use array pluck for data: data.rows.fieldName","- Col data can be component arrays for styled cells: Col(\"Status\", @Each(data.rows, \"item\", Tag(item.status, null, \"sm\", item.status == \"open\" ? \"success\" : \"danger\")))","- Row actions: Col(\"Actions\", @Each(data.rows, \"t\", Button(\"Edit\", Action([@Set($showEdit, true), @Set($editId, t.id)]))))","- Sortable: sorted = @Sort(data.rows, $sortField, \"desc\"). Bind $sortField to Select. Use sorted.fieldName for Col data","- Searchable: filtered = @Filter(data.rows, \"title\", \"contains\", $search). Bind $search to Input","- Chain sort + filter: filtered = @Filter(...) then sorted = @Sort(filtered, ...) — use sorted for both Table and Charts","- Empty state: @Count(data.rows) > 0 ? Table([...]) : TextContent(\"No data yet\")"]},{"name":"Charts (2D)","components":["BarChart","LineChart","AreaChart","RadarChart","HorizontalBarChart","Series"],"notes":["- Charts accept column arrays: LineChart(labels, [Series(\"Name\", values)]). Use array pluck: LineChart(data.rows.day, [Series(\"Views\", data.rows.views)])","- Use Cards to wrap charts with CardHeader for titled sections","- Chart + Table from same source: use @Sort or @Filter result for both LineChart and Table Col data","- Multiple chart views: use Tabs — Tabs([TabItem(\"line\", \"Line\", [LineChart(...)]), TabItem(\"bar\", \"Bar\", [BarChart(...)])])"]},{"name":"Charts (1D)","components":["PieChart","RadialChart","SingleStackedBarChart","Slice"],"notes":["- PieChart and BarChart need NUMBERS, not objects. For list data, use @Count(@Filter(...)) to aggregate:","- PieChart from list: `PieChart([\"Low\", \"Med\", \"High\"], [@Count(@Filter(data.rows, \"priority\", \"==\", \"low\")), @Count(@Filter(data.rows, \"priority\", \"==\", \"medium\")), @Count(@Filter(data.rows, \"priority\", \"==\", \"high\"))], \"donut\")`","- KPI from count: `TextContent(\"\" + @Count(@Filter(data.rows, \"status\", \"==\", \"open\")), \"large-heavy\")`"]},{"name":"Charts (Scatter)","components":["ScatterChart","ScatterSeries","Point"]},{"name":"Forms","components":["Form","FormControl","Label","Input","TextArea","Select","SelectItem","DatePicker","Slider","CheckBoxGroup","CheckBoxItem","RadioGroup","RadioItem","SwitchGroup","SwitchItem","Chips","ChipItem","OptionCards","OptionCard"],"notes":["- Chips: compact single/multiple selection pills. Use ChipItem references for each option.","- OptionCards: larger selectable cards with title, subtitle and an optional Icon or Image on top. Use OptionCard references for each option.","- For Form fields, define EACH FormControl as its own reference — do NOT inline all controls in one array. This allows progressive field-by-field streaming.","- NEVER nest Form inside Form — each Form should be a standalone container.","- Form requires explicit buttons. Always pass a Buttons(...) reference as the third Form argument.","- rules is an optional object: {required: true, email: true, minLength: 8, maxLength: 100}","- Available rules: required, email, min, max, minLength, maxLength, pattern, url, numeric","- The renderer shows error messages automatically — do NOT generate error text in the UI","- Conditional fields: $country == \"US\" ? stateField : $country == \"UK\" ? postcodeField : addressField","- Edit form in Modal: Modal(\"Edit\", $showEdit, [Form(\"edit\", Buttons([saveBtn, cancelBtn]), [fields...])]). Save button should include @Set($showEdit, false) to close modal."]},{"name":"Buttons","components":["Button","Buttons","IconButton"],"notes":["- Icon renders a lucide icon by kebab-case name; it is also used as the icon of IconButton, IconText and OptionCard.","- Toggle in @Each: @Each(rows, \"t\", Button(t.status == \"open\" ? \"Close\" : \"Reopen\", Action([...])))"]},{"name":"Data Display","components":["TagBlock","Tag","Icon","EntityList","ListBlock","ListItem"],"notes":["- Color-mapped Tag: Tag(value, null, \"sm\", value == \"high\" ? \"danger\" : value == \"medium\" ? \"warning\" : \"neutral\")","- EntityList is a compact two-column list of { left, right } rows (e.g. name / value). size='default' also supports a header and footer row; size='small' does not.","- ListBlock is a numbered or image list of ListItem references. An action on ListItem is optional."]},{"name":"Cards","components":["SnippetCardBlock","SnippetCardItem","OverviewCardBlock","OverviewCardItem","ContextCardBlock","ContextCardItem","CompositeCardBlock","CompositeCardItem","VisualCardBlock","VisualCardItem","Text","BoldText","IconText","ImageText","ImageTextLarge","MetricIndicatorInline","MetricIndicatorWithStrikethrough"],"notes":["- Card blocks lay out 2+ items in a responsive grid (or carousel where supported). Every item in a block must have the same structure.","- SnippetCardItem: small card with lhs (IconText | ImageText) and optional rhs (Text | BoldText) — good for key/value facts.","- OverviewCardItem: small card with top (IconText | ImageText | Text) and optional bottom MetricIndicatorInline — good for KPIs.","- ContextCardItem: medium card with a title (string or Tag), body text and optional background image — good for summaries.","- CompositeCardItem: rich card with header, body array (Text, BoldText, MetricIndicatorInline, IconText, Image, charts, ListBlock, TagBlock, EntityList) and footer (price + Button) — good for products/offers.","- VisualCardItem: image-first card with a BoldText body and optional Tag.","- Text / BoldText / IconText / ImageText / ImageTextLarge / MetricIndicator* are the inline building blocks used INSIDE card items; do not place them directly in the root Card."]}],"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"Card":{"$ref":"#/$defs/Card"},"CardHeader":{"$ref":"#/$defs/CardHeader"},"TextContent":{"$ref":"#/$defs/TextContent"},"MarkDownRenderer":{"$ref":"#/$defs/MarkDownRenderer"},"Callout":{"$ref":"#/$defs/Callout"},"TextCallout":{"$ref":"#/$defs/TextCallout"},"Image":{"$ref":"#/$defs/Image"},"ImageBlock":{"$ref":"#/$defs/ImageBlock"},"ImageGallery":{"$ref":"#/$defs/ImageGallery"},"CodeBlock":{"$ref":"#/$defs/CodeBlock"},"InlineHeader":{"$ref":"#/$defs/InlineHeader"},"Table":{"$ref":"#/$defs/Table"},"Col":{"$ref":"#/$defs/Col"},"EditableTable":{"$ref":"#/$defs/EditableTable"},"BarChart":{"$ref":"#/$defs/BarChart"},"LineChart":{"$ref":"#/$defs/LineChart"},"AreaChart":{"$ref":"#/$defs/AreaChart"},"RadarChart":{"$ref":"#/$defs/RadarChart"},"HorizontalBarChart":{"$ref":"#/$defs/HorizontalBarChart"},"Series":{"$ref":"#/$defs/Series"},"PieChart":{"$ref":"#/$defs/PieChart"},"RadialChart":{"$ref":"#/$defs/RadialChart"},"SingleStackedBarChart":{"$ref":"#/$defs/SingleStackedBarChart"},"Slice":{"$ref":"#/$defs/Slice"},"ScatterChart":{"$ref":"#/$defs/ScatterChart"},"ScatterSeries":{"$ref":"#/$defs/ScatterSeries"},"Point":{"$ref":"#/$defs/Point"},"Form":{"$ref":"#/$defs/Form"},"FormControl":{"$ref":"#/$defs/FormControl"},"Label":{"$ref":"#/$defs/Label"},"Input":{"$ref":"#/$defs/Input"},"TextArea":{"$ref":"#/$defs/TextArea"},"Select":{"$ref":"#/$defs/Select"},"SelectItem":{"$ref":"#/$defs/SelectItem"},"DatePicker":{"$ref":"#/$defs/DatePicker"},"Slider":{"$ref":"#/$defs/Slider"},"CheckBoxGroup":{"$ref":"#/$defs/CheckBoxGroup"},"CheckBoxItem":{"$ref":"#/$defs/CheckBoxItem"},"RadioGroup":{"$ref":"#/$defs/RadioGroup"},"RadioItem":{"$ref":"#/$defs/RadioItem"},"SwitchGroup":{"$ref":"#/$defs/SwitchGroup"},"SwitchItem":{"$ref":"#/$defs/SwitchItem"},"ChipItem":{"$ref":"#/$defs/ChipItem"},"Chips":{"$ref":"#/$defs/Chips"},"OptionCard":{"$ref":"#/$defs/OptionCard"},"OptionCards":{"$ref":"#/$defs/OptionCards"},"Button":{"$ref":"#/$defs/Button"},"Buttons":{"$ref":"#/$defs/Buttons"},"IconButton":{"$ref":"#/$defs/IconButton"},"Stack":{"$ref":"#/$defs/Stack"},"Tabs":{"$ref":"#/$defs/Tabs"},"TabItem":{"$ref":"#/$defs/TabItem"},"Accordion":{"$ref":"#/$defs/Accordion"},"AccordionItem":{"$ref":"#/$defs/AccordionItem"},"Steps":{"$ref":"#/$defs/Steps"},"StepsItem":{"$ref":"#/$defs/StepsItem"},"Carousel":{"$ref":"#/$defs/Carousel"},"Separator":{"$ref":"#/$defs/Separator"},"TagBlock":{"$ref":"#/$defs/TagBlock"},"Tag":{"$ref":"#/$defs/Tag"},"Icon":{"$ref":"#/$defs/Icon"},"EntityList":{"$ref":"#/$defs/EntityList"},"ListBlock":{"$ref":"#/$defs/ListBlock"},"ListItem":{"$ref":"#/$defs/ListItem"},"Text":{"$ref":"#/$defs/Text"},"BoldText":{"$ref":"#/$defs/BoldText"},"IconText":{"$ref":"#/$defs/IconText"},"ImageText":{"$ref":"#/$defs/ImageText"},"ImageTextLarge":{"$ref":"#/$defs/ImageTextLarge"},"MetricIndicatorInline":{"$ref":"#/$defs/MetricIndicatorInline"},"MetricIndicatorWithStrikethrough":{"$ref":"#/$defs/MetricIndicatorWithStrikethrough"},"SnippetCardItem":{"$ref":"#/$defs/SnippetCardItem"},"SnippetCardBlock":{"$ref":"#/$defs/SnippetCardBlock"},"OverviewCardItem":{"$ref":"#/$defs/OverviewCardItem"},"OverviewCardBlock":{"$ref":"#/$defs/OverviewCardBlock"},"ContextCardItem":{"$ref":"#/$defs/ContextCardItem"},"ContextCardBlock":{"$ref":"#/$defs/ContextCardBlock"},"CompositeCardItem":{"$ref":"#/$defs/CompositeCardItem"},"CompositeCardBlock":{"$ref":"#/$defs/CompositeCardBlock"},"VisualCardItem":{"$ref":"#/$defs/VisualCardItem"},"VisualCardBlock":{"$ref":"#/$defs/VisualCardBlock"},"Modal":{"$ref":"#/$defs/Modal"}},"required":["Card","CardHeader","TextContent","MarkDownRenderer","Callout","TextCallout","Image","ImageBlock","ImageGallery","CodeBlock","InlineHeader","Table","Col","EditableTable","BarChart","LineChart","AreaChart","RadarChart","HorizontalBarChart","Series","PieChart","RadialChart","SingleStackedBarChart","Slice","ScatterChart","ScatterSeries","Point","Form","FormControl","Label","Input","TextArea","Select","SelectItem","DatePicker","Slider","CheckBoxGroup","CheckBoxItem","RadioGroup","RadioItem","SwitchGroup","SwitchItem","ChipItem","Chips","OptionCard","OptionCards","Button","Buttons","IconButton","Stack","Tabs","TabItem","Accordion","AccordionItem","Steps","StepsItem","Carousel","Separator","TagBlock","Tag","Icon","EntityList","ListBlock","ListItem","Text","BoldText","IconText","ImageText","ImageTextLarge","MetricIndicatorInline","MetricIndicatorWithStrikethrough","SnippetCardItem","SnippetCardBlock","OverviewCardItem","OverviewCardBlock","ContextCardItem","ContextCardBlock","CompositeCardItem","CompositeCardBlock","VisualCardItem","VisualCardBlock","Modal"],"additionalProperties":false,"$defs":{"Card":{"type":"object","properties":{"children":{"type":"array","items":{"anyOf":[{"$ref":"#/$defs/TextContent"},{"$ref":"#/$defs/MarkDownRenderer"},{"$ref":"#/$defs/CardHeader"},{"$ref":"#/$defs/Callout"},{"$ref":"#/$defs/TextCallout"},{"$ref":"#/$defs/CodeBlock"},{"$ref":"#/$defs/Image"},{"$ref":"#/$defs/ImageBlock"},{"$ref":"#/$defs/ImageGallery"},{"$ref":"#/$defs/Separator"},{"$ref":"#/$defs/HorizontalBarChart"},{"$ref":"#/$defs/RadarChart"},{"$ref":"#/$defs/PieChart"},{"$ref":"#/$defs/RadialChart"},{"$ref":"#/$defs/SingleStackedBarChart"},{"$ref":"#/$defs/ScatterChart"},{"$ref":"#/$defs/AreaChart"},{"$ref":"#/$defs/BarChart"},{"$ref":"#/$defs/LineChart"},{"$ref":"#/$defs/Table"},{"$ref":"#/$defs/TagBlock"},{"$ref":"#/$defs/Form"},{"$ref":"#/$defs/Buttons"},{"$ref":"#/$defs/IconButton"},{"$ref":"#/$defs/Steps"},{"$ref":"#/$defs/InlineHeader"},{"$ref":"#/$defs/EntityList"},{"$ref":"#/$defs/EditableTable"},{"$ref":"#/$defs/SnippetCardBlock"},{"$ref":"#/$defs/OverviewCardBlock"},{"$ref":"#/$defs/ContextCardBlock"},{"$ref":"#/$defs/CompositeCardBlock"},{"$ref":"#/$defs/VisualCardBlock"},{"$ref":"#/$defs/Tabs"},{"$ref":"#/$defs/Carousel"},{"$ref":"#/$defs/Stack"}]}},"variant":{"type":"string","enum":["card","sunk","clear"]},"direction":{"type":"string","enum":["row","column"]},"gap":{"type":"string","enum":["none","xs","s","m","l","xl","2xl"]},"align":{"type":"string","enum":["start","center","end","stretch","baseline"]},"justify":{"type":"string","enum":["start","center","end","between","around","evenly"]},"wrap":{"type":"boolean"}},"required":["children"],"additionalProperties":false,"description":"Styled container. variant: \"card\" (default, elevated) | \"sunk\" (recessed) | \"clear\" (transparent). Always full width. Accepts all Stack flex params (default: direction \"column\"). Cards flex to share space in row/wrap layouts."},"TextContent":{"type":"object","properties":{"text":{"type":"string"},"size":{"type":"string","enum":["small","default","large","small-heavy","large-heavy"]}},"required":["text"],"additionalProperties":false,"description":"Text block. Supports markdown. Optional size: \"small\" | \"default\" | \"large\" | \"small-heavy\" | \"large-heavy\"."},"MarkDownRenderer":{"type":"object","properties":{"textMarkdown":{"type":"string"},"variant":{"type":"string","enum":["clear","card","sunk"]}},"required":["textMarkdown"],"additionalProperties":false,"description":"Renders markdown text with optional container variant"},"CardHeader":{"type":"object","properties":{"title":{"type":"string"},"subtitle":{"type":"string"}},"additionalProperties":false,"description":"Header with optional title and subtitle"},"Callout":{"type":"object","properties":{"variant":{"type":"string","enum":["info","warning","error","success","neutral"]},"title":{"type":"string"},"description":{"type":"string"},"visible":{"type":"boolean"}},"required":["variant","title","description"],"additionalProperties":false,"description":"Callout banner. Optional visible is a reactive $boolean — auto-dismisses after 3s by setting $visible to false."},"TextCallout":{"type":"object","properties":{"variant":{"type":"string","enum":["neutral","info","warning","success","danger"]},"title":{"type":"string"},"description":{"type":"string"}},"additionalProperties":false,"description":"Text callout with variant, title, and description"},"CodeBlock":{"type":"object","properties":{"language":{"type":"string"},"codeString":{"type":"string"}},"required":["language","codeString"],"additionalProperties":false,"description":"Syntax-highlighted code block"},"Image":{"type":"object","properties":{"alt":{"type":"string"},"src":{"type":"string"}},"required":["alt"],"additionalProperties":false,"description":"Image with alt text and optional URL"},"ImageBlock":{"type":"object","properties":{"src":{"type":"string"},"alt":{"type":"string"}},"required":["src"],"additionalProperties":false,"description":"Image block with loading state"},"ImageGallery":{"type":"object","properties":{"images":{"type":"array","items":{"type":"object","properties":{"src":{"type":"string"},"alt":{"type":"string"},"details":{"type":"string"}},"required":["src"],"additionalProperties":false}}},"required":["images"],"additionalProperties":false,"description":"Gallery grid of images with modal preview"},"Separator":{"type":"object","properties":{"orientation":{"type":"string","enum":["horizontal","vertical"]},"decorative":{"type":"boolean"}},"additionalProperties":false,"description":"Visual divider between content sections"},"HorizontalBarChart":{"type":"object","properties":{"labels":{"type":"array","items":{"type":"string"}},"series":{"type":"array","items":{"$ref":"#/$defs/Series"}},"variant":{"type":"string","enum":["grouped","stacked"]},"xLabel":{"type":"string"},"yLabel":{"type":"string"}},"required":["labels","series"],"additionalProperties":false,"description":"Horizontal bars; prefer when category labels are long or for ranked lists"},"Series":{"type":"object","properties":{"category":{"type":"string"},"values":{"type":"array","items":{"type":"number"}}},"required":["category","values"],"additionalProperties":false,"description":"One data series"},"RadarChart":{"type":"object","properties":{"labels":{"type":"array","items":{"type":"string"}},"series":{"type":"array","items":{"$ref":"#/$defs/Series"}}},"required":["labels","series"],"additionalProperties":false,"description":"Spider/web chart; use for comparing multiple variables across one or more entities"},"PieChart":{"type":"object","properties":{"labels":{"type":"array","items":{"type":"string"}},"values":{"type":"array","items":{"type":"number"}},"variant":{"type":"string","enum":["pie","donut"]},"appearance":{"type":"string","enum":["circular","semiCircular"]}},"required":["labels","values"],"additionalProperties":false,"description":"Circular slices; use plucked arrays: PieChart(data.categories, data.values)"},"RadialChart":{"type":"object","properties":{"labels":{"type":"array","items":{"type":"string"}},"values":{"type":"array","items":{"type":"number"}}},"required":["labels","values"],"additionalProperties":false,"description":"Radial bars; use plucked arrays: RadialChart(data.categories, data.values)"},"SingleStackedBarChart":{"type":"object","properties":{"labels":{"type":"array","items":{"type":"string"}},"values":{"type":"array","items":{"type":"number"}}},"required":["labels","values"],"additionalProperties":false,"description":"Single horizontal stacked bar; use plucked arrays: SingleStackedBarChart(data.categories, data.values)"},"ScatterChart":{"type":"object","properties":{"datasets":{"type":"array","items":{"$ref":"#/$defs/ScatterSeries"}},"xLabel":{"type":"string"},"yLabel":{"type":"string"}},"required":["datasets"],"additionalProperties":false,"description":"X/Y scatter plot; use for correlations, distributions, and clustering"},"ScatterSeries":{"type":"object","properties":{"name":{"type":"string"},"points":{"type":"array","items":{"$ref":"#/$defs/Point"}}},"required":["name","points"],"additionalProperties":false,"description":"Named dataset"},"Point":{"type":"object","properties":{"x":{"type":"number"},"y":{"type":"number"},"z":{"type":"number"}},"required":["x","y"],"additionalProperties":false,"description":"Data point with numeric coordinates"},"AreaChart":{"type":"object","properties":{"labels":{"type":"array","items":{"type":"string"}},"series":{"type":"array","items":{"$ref":"#/$defs/Series"}},"variant":{"type":"string","enum":["linear","natural","step"]},"xLabel":{"type":"string"},"yLabel":{"type":"string"},"height":{"type":"number"}},"required":["labels","series"],"additionalProperties":false,"description":"Filled area under lines; use for cumulative totals or volume trends over time"},"BarChart":{"type":"object","properties":{"labels":{"type":"array","items":{"type":"string"}},"series":{"type":"array","items":{"$ref":"#/$defs/Series"}},"variant":{"type":"string","enum":["grouped","stacked"]},"xLabel":{"type":"string"},"yLabel":{"type":"string"},"height":{"type":"number"}},"required":["labels","series"],"additionalProperties":false,"description":"Vertical bars; use for comparing values across categories with one or more series"},"LineChart":{"type":"object","properties":{"labels":{"type":"array","items":{"type":"string"}},"series":{"type":"array","items":{"$ref":"#/$defs/Series"}},"variant":{"type":"string","enum":["linear","natural","step"]},"xLabel":{"type":"string"},"yLabel":{"type":"string"},"height":{"type":"number"}},"required":["labels","series"],"additionalProperties":false,"description":"Lines over categories; use for trends and continuous data over time"},"Table":{"type":"object","properties":{"columns":{"type":"array","items":{"$ref":"#/$defs/Col"}}},"required":["columns"],"additionalProperties":false,"description":"Data table — column-oriented. Each Col holds its own data array."},"Col":{"type":"object","properties":{"label":{"type":"string"},"data":{},"type":{"type":"string","enum":["string","number","action"]}},"required":["label","data"],"additionalProperties":false,"description":"Column definition — holds label + data array"},"TagBlock":{"type":"object","properties":{"tags":{"type":"array","items":{"type":"string"}},"size":{"type":"string","enum":["sm","md","lg"]}},"required":["tags"],"additionalProperties":false,"description":"tags is an array of strings; optional size sm | md | lg"},"Form":{"type":"object","properties":{"name":{"type":"string"},"buttons":{"$ref":"#/$defs/Buttons"},"fields":{"default":[],"type":"array","items":{"$ref":"#/$defs/FormControl"}}},"required":["name","buttons","fields"],"additionalProperties":false,"description":"Form container with fields and explicit action buttons"},"Buttons":{"type":"object","properties":{"buttons":{"type":"array","items":{"$ref":"#/$defs/Button"}},"direction":{"type":"string","enum":["row","column"]}},"required":["buttons"],"additionalProperties":false,"description":"Group of Button components. direction: \"row\" (default) | \"column\"."},"Button":{"type":"object","properties":{"label":{"type":"string"},"action":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","const":"open_url"},"url":{"type":"string"}},"required":["type","url"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string","const":"continue_conversation"},"context":{"type":"string"}},"required":["type"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string"},"params":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["type"],"additionalProperties":false}]},"variant":{"type":"string","enum":["primary","secondary","tertiary"]},"type":{"type":"string","enum":["normal","destructive"]},"size":{"type":"string","enum":["extra-small","small","medium","large"]}},"required":["label"],"additionalProperties":false,"description":"Clickable button"},"FormControl":{"type":"object","properties":{"label":{"type":"string"},"input":{"anyOf":[{"$ref":"#/$defs/Input"},{"$ref":"#/$defs/TextArea"},{"$ref":"#/$defs/Select"},{"$ref":"#/$defs/DatePicker"},{"$ref":"#/$defs/Slider"},{"$ref":"#/$defs/CheckBoxGroup"},{"$ref":"#/$defs/RadioGroup"},{"$ref":"#/$defs/Chips"},{"$ref":"#/$defs/OptionCards"}]},"hint":{"type":"string"}},"required":["label","input"],"additionalProperties":false,"description":"Field with label, input component, and optional hint text"},"Input":{"type":"object","properties":{"name":{"type":"string"},"placeholder":{"type":"string"},"type":{"type":"string","enum":["text","email","password","number","url"]},"rules":{"type":"object","properties":{"required":{"type":"boolean"},"email":{"type":"boolean"},"url":{"type":"boolean"},"numeric":{"type":"boolean"},"min":{"type":"number"},"max":{"type":"number"},"minLength":{"type":"number"},"maxLength":{"type":"number"},"pattern":{"type":"string"}},"additionalProperties":false},"value":{"type":"string"}},"required":["name"],"additionalProperties":false},"TextArea":{"type":"object","properties":{"name":{"type":"string"},"placeholder":{"type":"string"},"rows":{"type":"number"},"rules":{"type":"object","properties":{"required":{"type":"boolean"},"email":{"type":"boolean"},"url":{"type":"boolean"},"numeric":{"type":"boolean"},"min":{"type":"number"},"max":{"type":"number"},"minLength":{"type":"number"},"maxLength":{"type":"number"},"pattern":{"type":"string"}},"additionalProperties":false},"value":{"type":"string"}},"required":["name"],"additionalProperties":false},"Select":{"type":"object","properties":{"name":{"type":"string"},"items":{"type":"array","items":{"$ref":"#/$defs/SelectItem"}},"placeholder":{"type":"string"},"rules":{"type":"object","properties":{"required":{"type":"boolean"},"email":{"type":"boolean"},"url":{"type":"boolean"},"numeric":{"type":"boolean"},"min":{"type":"number"},"max":{"type":"number"},"minLength":{"type":"number"},"maxLength":{"type":"number"},"pattern":{"type":"string"}},"additionalProperties":false},"value":{"type":"string"},"size":{"type":"string","enum":["small","medium","large"]}},"required":["name","items"],"additionalProperties":false},"SelectItem":{"type":"object","properties":{"value":{"type":"string"},"label":{"type":"string"}},"required":["value","label"],"additionalProperties":false,"description":"Option for Select"},"DatePicker":{"type":"object","properties":{"name":{"type":"string"},"mode":{"type":"string","enum":["single","range"]},"rules":{"type":"object","properties":{"required":{"type":"boolean"},"email":{"type":"boolean"},"url":{"type":"boolean"},"numeric":{"type":"boolean"},"min":{"type":"number"},"max":{"type":"number"},"minLength":{"type":"number"},"maxLength":{"type":"number"},"pattern":{"type":"string"}},"additionalProperties":false},"value":{}},"required":["name"],"additionalProperties":false},"Slider":{"type":"object","properties":{"name":{"type":"string"},"variant":{"type":"string","enum":["continuous","discrete"]},"min":{"type":"number"},"max":{"type":"number"},"step":{"type":"number"},"defaultValue":{"type":"array","items":{"type":"number"}},"label":{"type":"string"},"rules":{"type":"object","properties":{"required":{"type":"boolean"},"email":{"type":"boolean"},"url":{"type":"boolean"},"numeric":{"type":"boolean"},"min":{"type":"number"},"max":{"type":"number"},"minLength":{"type":"number"},"maxLength":{"type":"number"},"pattern":{"type":"string"}},"additionalProperties":false},"value":{"type":"array","items":{"type":"number"}}},"required":["name","variant","min","max"],"additionalProperties":false,"description":"Numeric slider input; supports continuous and discrete (stepped) variants"},"CheckBoxGroup":{"type":"object","properties":{"name":{"type":"string"},"items":{"type":"array","items":{"$ref":"#/$defs/CheckBoxItem"}},"rules":{"type":"object","properties":{"required":{"type":"boolean"},"email":{"type":"boolean"},"url":{"type":"boolean"},"numeric":{"type":"boolean"},"min":{"type":"number"},"max":{"type":"number"},"minLength":{"type":"number"},"maxLength":{"type":"number"},"pattern":{"type":"string"}},"additionalProperties":false},"value":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"boolean"}}},"required":["name","items"],"additionalProperties":false},"CheckBoxItem":{"type":"object","properties":{"label":{"type":"string"},"description":{"type":"string"},"name":{"type":"string"},"defaultChecked":{"type":"boolean"}},"required":["label","description","name"],"additionalProperties":false},"RadioGroup":{"type":"object","properties":{"name":{"type":"string"},"items":{"type":"array","items":{"$ref":"#/$defs/RadioItem"}},"defaultValue":{"type":"string"},"rules":{"type":"object","properties":{"required":{"type":"boolean"},"email":{"type":"boolean"},"url":{"type":"boolean"},"numeric":{"type":"boolean"},"min":{"type":"number"},"max":{"type":"number"},"minLength":{"type":"number"},"maxLength":{"type":"number"},"pattern":{"type":"string"}},"additionalProperties":false},"value":{"type":"string"}},"required":["name","items"],"additionalProperties":false},"RadioItem":{"type":"object","properties":{"label":{"type":"string"},"description":{"type":"string"},"value":{"type":"string"}},"required":["label","description","value"],"additionalProperties":false},"Chips":{"type":"object","properties":{"name":{"type":"string"},"type":{"default":"multiple","type":"string","enum":["single","multiple"]},"items":{"default":[],"type":"array","items":{"$ref":"#/$defs/ChipItem"}},"rules":{"type":"object","properties":{"required":{"type":"boolean"},"email":{"type":"boolean"},"url":{"type":"boolean"},"numeric":{"type":"boolean"},"min":{"type":"number"},"max":{"type":"number"},"minLength":{"type":"number"},"maxLength":{"type":"number"},"pattern":{"type":"string"}},"additionalProperties":false},"defaultValue":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]}},"required":["name","type","items"],"additionalProperties":false,"description":"A form field of compact selectable chips for choosing one or many short options; the selection is stored under `name`."},"ChipItem":{"type":"object","properties":{"value":{"type":"string"},"label":{"type":"string"},"icon":{"$ref":"#/$defs/Icon"},"disabled":{"type":"boolean"}},"required":["value","label"],"additionalProperties":false,"description":"A single selectable chip inside a Chips group, with a value, label and optional icon."},"Icon":{"type":"object","properties":{"name":{"type":"string"},"category":{"type":"string"}},"required":["name"],"additionalProperties":false,"description":"A lucide icon by kebab-case name (e.g. 'circle-check'). Optional category picks a topical fallback when the name doesn't resolve."},"OptionCards":{"type":"object","properties":{"name":{"type":"string"},"type":{"default":"single","type":"string","enum":["single","multiple"]},"items":{"default":[],"type":"array","items":{"$ref":"#/$defs/OptionCard"}},"rules":{"type":"object","properties":{"required":{"type":"boolean"},"email":{"type":"boolean"},"url":{"type":"boolean"},"numeric":{"type":"boolean"},"min":{"type":"number"},"max":{"type":"number"},"minLength":{"type":"number"},"maxLength":{"type":"number"},"pattern":{"type":"string"}},"additionalProperties":false},"defaultValue":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]}},"required":["name","type","items"],"additionalProperties":false,"description":"A form field of selectable cards (title, optional subtitle, optional icon or image) laid out in a responsive grid for choosing one or many options; the selection is stored under `name`."},"OptionCard":{"type":"object","properties":{"value":{"type":"string"},"title":{"type":"string"},"subtitle":{"type":"string"},"topContent":{"anyOf":[{"$ref":"#/$defs/Icon"},{"$ref":"#/$defs/Image"}]},"disabled":{"type":"boolean"}},"required":["value","title"],"additionalProperties":false,"description":"A single selectable card inside an OptionCards group, with a value, title, optional subtitle and an optional Icon or Image on top."},"IconButton":{"type":"object","properties":{"name":{"type":"string"},"icon":{"$ref":"#/$defs/Icon"},"action":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","const":"open_url"},"url":{"type":"string"}},"required":["type","url"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string","const":"continue_conversation"},"context":{"type":"string"}},"required":["type"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string"},"params":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["type"],"additionalProperties":false}]},"variant":{"type":"string","enum":["primary","secondary","tertiary"]},"size":{"type":"string","enum":["extra-small","small","medium","large"]},"shape":{"type":"string","enum":["square","circle"]}},"required":["name","icon"],"additionalProperties":false,"description":"Icon-only button. name is the accessible label and the action label; icon is an Icon; action fires on click."},"Steps":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/$defs/StepsItem"}}},"required":["items"],"additionalProperties":false,"description":"Step-by-step guide"},"StepsItem":{"type":"object","properties":{"title":{"type":"string"},"details":{"type":"string"}},"required":["title","details"],"additionalProperties":false,"description":"title and details text for one step"},"InlineHeader":{"type":"object","properties":{"heading":{"type":"string"},"description":{"type":"string"}},"required":["heading"],"additionalProperties":false,"description":"Compact section heading with an optional one-line description, for use inside cards."},"EntityList":{"type":"object","properties":{"rows":{"default":[],"type":"array","items":{"type":"object","properties":{"left":{"type":"string"},"right":{"type":"string"},"rightVariant":{"default":"text","type":"string","enum":["text","number"]}},"required":["left","right","rightVariant"],"additionalProperties":false}},"size":{"default":"default","type":"string","enum":["small","default"]},"header":{"type":"object","properties":{"left":{"type":"string"},"right":{"type":"string"},"rightVariant":{"default":"text","type":"string","enum":["text","number"]}},"required":["left","right","rightVariant"],"additionalProperties":false},"footer":{"type":"object","properties":{"left":{"type":"string"},"right":{"type":"string"},"rightVariant":{"default":"text","type":"string","enum":["text","number"]}},"required":["left","right","rightVariant"],"additionalProperties":false}},"required":["rows","size"],"additionalProperties":false,"description":"Two-column key/value rows (left label, right value). size 'default' supports optional header and footer rows; rightVariant 'number' uses tabular numbers."},"EditableTable":{"type":"object","properties":{"name":{"default":"","type":"string"},"columns":{"default":[],"type":"array","items":{"type":"object","properties":{"type":{"type":"string","enum":["text","number","date-single","select","url"]},"key":{"default":"default","type":"string"},"header":{"default":"","type":"string"},"width":{"type":"number"},"options":{"type":"array","items":{"type":"object","properties":{"value":{"type":"string"},"label":{"type":"string"}},"required":["value","label"],"additionalProperties":false}}},"required":["type","key","header"],"additionalProperties":false}},"data":{"default":[],"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"values":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}}},"required":["id","values"],"additionalProperties":false}}},"required":["name","columns","data"],"additionalProperties":false,"description":"Spreadsheet-like table whose cells the user can edit inline (text, number, url, date, select columns); edits are saved back as a form field"},"SnippetCardBlock":{"type":"object","properties":{"items":{"minItems":2,"type":"array","items":{"$ref":"#/$defs/SnippetCardItem"}},"layout":{"default":"grid","type":"string","enum":["grid"]},"responsive":{"default":true,"type":"boolean"},"action":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","const":"open_url"},"url":{"type":"string"}},"required":["type","url"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string","const":"continue_conversation"},"context":{"type":"string"}},"required":["type"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string"},"params":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["type"],"additionalProperties":false}]},"gap":{"anyOf":[{"type":"number"},{"type":"string"}]}},"required":["items","layout","responsive"],"additionalProperties":false,"description":"A responsive grid of compact label/value cards (2 per row) for showing several short facts side by side; optionally clickable with a shared action."},"SnippetCardItem":{"type":"object","properties":{"id":{"type":"string"},"lhs":{"anyOf":[{"$ref":"#/$defs/IconText"},{"$ref":"#/$defs/ImageText"}]},"rhs":{"anyOf":[{"$ref":"#/$defs/Text"},{"$ref":"#/$defs/BoldText"}]}},"required":["lhs"],"additionalProperties":false,"description":"One row-style snippet card: a label on the left (IconText or ImageText) and an optional value on the right (Text or BoldText)."},"IconText":{"type":"object","properties":{"icon":{"$ref":"#/$defs/Icon"},"iconVariant":{"default":"neutral","type":"string","enum":["neutral","info","success","warning","danger","inverted","filled","soft"]},"iconSize":{"default":"m","type":"string","enum":["xs","s","m","l","xl","sm","md","lg"]},"title":{"type":"string"},"subtitle":{"type":"string"},"bold":{"default":false,"type":"boolean"},"layout":{"default":"horizontal","type":"string","enum":["horizontal","vertical"]}},"required":["icon","iconVariant","iconSize","title","bold","layout"],"additionalProperties":false,"description":"An icon badge with a title and optional subtitle, laid out horizontally or vertically. iconVariant sets the badge color."},"ImageText":{"type":"object","properties":{"src":{"type":"string"},"alt":{"type":"string"},"title":{"type":"string"},"subtitle":{"type":"string"},"bold":{"default":false,"type":"boolean"},"layout":{"default":"horizontal","type":"string","enum":["horizontal","vertical"]},"imageSize":{"type":"number"}},"required":["src","title","bold","layout"],"additionalProperties":false,"description":"A small square image (thumbnail/avatar) with a title and optional subtitle. src must be a real image URL."},"Text":{"type":"object","properties":{"variant":{"default":"text","type":"string","enum":["text","number"]},"value":{"type":"string"},"subtext":{"type":"string"},"subtextVariant":{"default":"text","type":"string","enum":["text","number","metric"]},"size":{"default":"sm","type":"string","enum":["xs","sm","md","lg"]}},"required":["variant","value","subtextVariant","size"],"additionalProperties":false,"description":"Plain text line with optional subtext. variant 'number' uses tabular number styling; subtextVariant 'metric' colors a leading +/- subtext green/red."},"BoldText":{"type":"object","properties":{"variant":{"default":"text","type":"string","enum":["text","number"]},"value":{"type":"string"},"subtext":{"type":"string"},"subtextVariant":{"default":"text","type":"string","enum":["text","number","metric"]},"size":{"default":"sm","type":"string","enum":["xs","sm","md","lg"]}},"required":["variant","value","subtextVariant","size"],"additionalProperties":false,"description":"Emphasized (bold) text line with optional subtext. variant 'number' uses tabular number styling; subtextVariant 'metric' colors a leading +/- subtext green/red."},"OverviewCardBlock":{"type":"object","properties":{"items":{"minItems":2,"type":"array","items":{"$ref":"#/$defs/OverviewCardItem"}},"layout":{"default":"grid","type":"string","enum":["grid","carousel"]},"responsive":{"default":true,"type":"boolean"},"action":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","const":"open_url"},"url":{"type":"string"}},"required":["type","url"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string","const":"continue_conversation"},"context":{"type":"string"}},"required":["type"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string"},"params":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["type"],"additionalProperties":false}]},"gap":{"anyOf":[{"type":"number"},{"type":"string"}]}},"required":["items","layout","responsive"],"additionalProperties":false,"description":"A grid or horizontal carousel of compact overview cards, each with a heading (icon/image/text) on top and an inline metric below; optionally clickable with a shared action."},"OverviewCardItem":{"type":"object","properties":{"id":{"type":"string"},"top":{"anyOf":[{"$ref":"#/$defs/IconText"},{"$ref":"#/$defs/ImageText"},{"$ref":"#/$defs/Text"}]},"bottom":{"$ref":"#/$defs/MetricIndicatorInline"}},"required":["top"],"additionalProperties":false,"description":"One overview card: a heading slot at the top (IconText, ImageText or Text) and an optional MetricIndicatorInline at the bottom."},"MetricIndicatorInline":{"type":"object","properties":{"value":{"type":"string"},"subtext":{"type":"string"},"trend":{"type":"object","properties":{"direction":{"type":"string","enum":["up","down"]},"value":{"type":"number"}},"required":["direction","value"],"additionalProperties":false}},"required":["value"],"additionalProperties":false,"description":"Headline metric value with an optional +/- percentage trend and subtext, all on one line."},"ContextCardBlock":{"type":"object","properties":{"items":{"minItems":2,"type":"array","items":{"$ref":"#/$defs/ContextCardItem"}},"layout":{"default":"grid","type":"string","enum":["grid","carousel"]},"responsive":{"default":true,"type":"boolean"},"action":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","const":"open_url"},"url":{"type":"string"}},"required":["type","url"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string","const":"continue_conversation"},"context":{"type":"string"}},"required":["type"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string"},"params":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["type"],"additionalProperties":false}]},"gap":{"anyOf":[{"type":"number"},{"type":"string"}]}},"required":["items","layout","responsive"],"additionalProperties":false,"description":"A grid or carousel of compact tinted context cards (title or tag plus a short bold body); an optional action makes every card clickable."},"ContextCardItem":{"type":"object","properties":{"id":{"type":"string"},"title":{"anyOf":[{"type":"string"},{"$ref":"#/$defs/Tag"}]},"body":{"type":"string"},"bgColor":{"type":"string","enum":["gray"]},"bgImageSrc":{"type":"string"},"bgImageAlt":{"type":"string"}},"required":["title"],"additionalProperties":false,"description":"A single card inside a ContextCardBlock: a title (plain string or Tag), an optional markdown body, and an optional gray tint or background image."},"Tag":{"type":"object","properties":{"text":{"type":"string"},"icon":{"$ref":"#/$defs/Icon"},"size":{"type":"string","enum":["sm","md","lg"]},"variant":{"type":"string","enum":["neutral","info","success","warning","danger"]}},"required":["text"],"additionalProperties":false,"description":"Styled tag/badge with optional Icon and variant"},"CompositeCardBlock":{"type":"object","properties":{"items":{"minItems":2,"type":"array","items":{"$ref":"#/$defs/CompositeCardItem"}},"layout":{"default":"grid","type":"string","enum":["grid","carousel"]},"responsive":{"default":true,"type":"boolean"},"action":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","const":"open_url"},"url":{"type":"string"}},"required":["type","url"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string","const":"continue_conversation"},"context":{"type":"string"}},"required":["type"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string"},"params":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["type"],"additionalProperties":false}]},"gap":{"anyOf":[{"type":"number"},{"type":"string"}]}},"required":["items","layout","responsive"],"additionalProperties":false,"description":"A two-per-row grid or carousel of rich cards, each with an optional header, stacked body content (text, metrics, charts, lists, tags) and a price/button footer; an optional action makes every card clickable."},"CompositeCardItem":{"type":"object","properties":{"id":{"type":"string"},"header":{"anyOf":[{"$ref":"#/$defs/IconText"},{"$ref":"#/$defs/ImageText"},{"$ref":"#/$defs/ImageTextLarge"},{"$ref":"#/$defs/Text"},{"$ref":"#/$defs/Image"}]},"body":{"default":[],"type":"array","items":{"anyOf":[{"$ref":"#/$defs/Text"},{"$ref":"#/$defs/BoldText"},{"$ref":"#/$defs/MetricIndicatorInline"},{"$ref":"#/$defs/IconText"},{"$ref":"#/$defs/Image"},{"$ref":"#/$defs/AreaChart"},{"$ref":"#/$defs/BarChart"},{"$ref":"#/$defs/LineChart"},{"$ref":"#/$defs/ListBlock"},{"$ref":"#/$defs/TagBlock"},{"$ref":"#/$defs/EntityList"}]}},"footer":{"type":"object","properties":{"price":{"anyOf":[{"$ref":"#/$defs/BoldText"},{"$ref":"#/$defs/MetricIndicatorWithStrikethrough"}]},"button":{"$ref":"#/$defs/Button"}},"additionalProperties":false}},"required":["body"],"additionalProperties":false,"description":"A single card inside a CompositeCardBlock: an optional header (icon/image/text), a stack of body elements (text, metrics, charts, lists, tags), and an optional price/button footer."},"ImageTextLarge":{"type":"object","properties":{"src":{"type":"string"},"alt":{"type":"string"},"title":{"type":"string"},"subtitle":{"type":"string"},"bold":{"default":false,"type":"boolean"}},"required":["src","title","bold"],"additionalProperties":false,"description":"A full-width banner image above a bold title and optional subtitle. src must be a real image URL."},"ListBlock":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/$defs/ListItem"}},"variant":{"type":"string","enum":["number","image"]},"size":{"type":"string","enum":["default","small"]}},"required":["items"],"additionalProperties":false,"description":"A list of items with number or image indicators. Each item can optionally have an action. size small renders a compact list."},"ListItem":{"type":"object","properties":{"title":{"type":"string"},"subtitle":{"type":"string"},"image":{"type":"object","properties":{"src":{"type":"string"},"alt":{"type":"string"}},"required":["src","alt"],"additionalProperties":false},"actionLabel":{"type":"string"},"action":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","const":"open_url"},"url":{"type":"string"}},"required":["type","url"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string","const":"continue_conversation"},"context":{"type":"string"}},"required":["type"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string"},"params":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["type"],"additionalProperties":false}]}},"required":["title"],"additionalProperties":false,"description":"Item in a ListBlock — displays a title with an optional subtitle and image. When action is provided, the item becomes clickable."},"MetricIndicatorWithStrikethrough":{"type":"object","properties":{"value":{"type":"string"},"subtext":{"type":"string"},"previousValue":{"type":"string"},"trend":{"type":"object","properties":{"direction":{"type":"string","enum":["up","down"]},"value":{"type":"number"}},"required":["direction","value"],"additionalProperties":false}},"required":["value"],"additionalProperties":false,"description":"Headline metric value with an optional struck-through previousValue, a +/- percentage trend, and subtext below."},"VisualCardBlock":{"type":"object","properties":{"items":{"minItems":2,"type":"array","items":{"$ref":"#/$defs/VisualCardItem"}},"layout":{"default":"grid","type":"string","enum":["grid","carousel"]},"responsive":{"default":true,"type":"boolean"},"action":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","const":"open_url"},"url":{"type":"string"}},"required":["type","url"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string","const":"continue_conversation"},"context":{"type":"string"}},"required":["type"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string"},"params":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["type"],"additionalProperties":false}]},"gap":{"anyOf":[{"type":"number"},{"type":"string"}]}},"required":["items","layout","responsive"],"additionalProperties":false,"description":"A grid or carousel of photo-first cards: a full-bleed background image with a tag on top and a bold text panel at the bottom; an optional action makes every card clickable."},"VisualCardItem":{"type":"object","properties":{"body":{"$ref":"#/$defs/BoldText"},"id":{"type":"string"},"bgImageSrc":{"type":"string"},"tag":{"$ref":"#/$defs/Tag"},"bgImageAlt":{"type":"string"}},"required":["body"],"additionalProperties":false,"description":"A single photo-first card inside a VisualCardBlock: a BoldText body panel, an optional Tag, and a background image (bgImageSrc must be a real URL; bgImageAlt is its alt text)."},"Tabs":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/$defs/TabItem"}}},"required":["items"],"additionalProperties":false,"description":"Tabbed container"},"TabItem":{"type":"object","properties":{"value":{"type":"string"},"trigger":{"type":"string"},"content":{"type":"array","items":{"anyOf":[{"$ref":"#/$defs/TextContent"},{"$ref":"#/$defs/MarkDownRenderer"},{"$ref":"#/$defs/CardHeader"},{"$ref":"#/$defs/Callout"},{"$ref":"#/$defs/TextCallout"},{"$ref":"#/$defs/CodeBlock"},{"$ref":"#/$defs/Image"},{"$ref":"#/$defs/ImageBlock"},{"$ref":"#/$defs/ImageGallery"},{"$ref":"#/$defs/Separator"},{"$ref":"#/$defs/HorizontalBarChart"},{"$ref":"#/$defs/RadarChart"},{"$ref":"#/$defs/PieChart"},{"$ref":"#/$defs/RadialChart"},{"$ref":"#/$defs/SingleStackedBarChart"},{"$ref":"#/$defs/ScatterChart"},{"$ref":"#/$defs/AreaChart"},{"$ref":"#/$defs/BarChart"},{"$ref":"#/$defs/LineChart"},{"$ref":"#/$defs/Table"},{"$ref":"#/$defs/TagBlock"},{"$ref":"#/$defs/Form"},{"$ref":"#/$defs/Buttons"},{"$ref":"#/$defs/IconButton"},{"$ref":"#/$defs/Steps"},{"$ref":"#/$defs/InlineHeader"},{"$ref":"#/$defs/EntityList"},{"$ref":"#/$defs/EditableTable"},{"$ref":"#/$defs/SnippetCardBlock"},{"$ref":"#/$defs/OverviewCardBlock"},{"$ref":"#/$defs/ContextCardBlock"},{"$ref":"#/$defs/CompositeCardBlock"},{"$ref":"#/$defs/VisualCardBlock"}]}}},"required":["value","trigger","content"],"additionalProperties":false,"description":"value is unique id, trigger is tab label, content is array of components"},"Carousel":{"type":"object","properties":{"children":{"type":"array","items":{"type":"array","items":{"anyOf":[{"$ref":"#/$defs/TextContent"},{"$ref":"#/$defs/MarkDownRenderer"},{"$ref":"#/$defs/CardHeader"},{"$ref":"#/$defs/Callout"},{"$ref":"#/$defs/TextCallout"},{"$ref":"#/$defs/CodeBlock"},{"$ref":"#/$defs/Image"},{"$ref":"#/$defs/ImageBlock"},{"$ref":"#/$defs/ImageGallery"},{"$ref":"#/$defs/Separator"},{"$ref":"#/$defs/HorizontalBarChart"},{"$ref":"#/$defs/RadarChart"},{"$ref":"#/$defs/PieChart"},{"$ref":"#/$defs/RadialChart"},{"$ref":"#/$defs/SingleStackedBarChart"},{"$ref":"#/$defs/ScatterChart"},{"$ref":"#/$defs/AreaChart"},{"$ref":"#/$defs/BarChart"},{"$ref":"#/$defs/LineChart"},{"$ref":"#/$defs/Table"},{"$ref":"#/$defs/TagBlock"},{"$ref":"#/$defs/Form"},{"$ref":"#/$defs/Buttons"},{"$ref":"#/$defs/IconButton"},{"$ref":"#/$defs/Steps"},{"$ref":"#/$defs/InlineHeader"},{"$ref":"#/$defs/EntityList"},{"$ref":"#/$defs/EditableTable"},{"$ref":"#/$defs/SnippetCardBlock"},{"$ref":"#/$defs/OverviewCardBlock"},{"$ref":"#/$defs/ContextCardBlock"},{"$ref":"#/$defs/CompositeCardBlock"},{"$ref":"#/$defs/VisualCardBlock"}]}}},"variant":{"type":"string","enum":["card","sunk"]}},"required":["children"],"additionalProperties":false,"description":"Horizontal scrollable carousel"},"Stack":{"type":"object","properties":{"children":{"type":"array","items":{}},"direction":{"type":"string","enum":["row","column"]},"gap":{"type":"string","enum":["none","xs","s","m","l","xl","2xl"]},"align":{"type":"string","enum":["start","center","end","stretch","baseline"]},"justify":{"type":"string","enum":["start","center","end","between","around","evenly"]},"wrap":{"type":"boolean"}},"required":["children"],"additionalProperties":false,"description":"Flex container. direction: \"row\"|\"column\" (default \"column\"). gap: \"none\"|\"xs\"|\"s\"|\"m\"|\"l\"|\"xl\"|\"2xl\" (default \"m\"). align: \"start\"|\"center\"|\"end\"|\"stretch\"|\"baseline\". justify: \"start\"|\"center\"|\"end\"|\"between\"|\"around\"|\"evenly\"."},"Slice":{"type":"object","properties":{"category":{"type":"string"},"value":{"type":"number"}},"required":["category","value"],"additionalProperties":false,"description":"One slice with label and numeric value"},"Label":{"type":"object","properties":{"text":{"type":"string"}},"required":["text"],"additionalProperties":false,"description":"Text label"},"SwitchGroup":{"type":"object","properties":{"name":{"type":"string"},"items":{"type":"array","items":{"$ref":"#/$defs/SwitchItem"}},"variant":{"type":"string","enum":["clear","card","sunk"]},"value":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"boolean"}}},"required":["name","items"],"additionalProperties":false,"description":"Group of switch toggles"},"SwitchItem":{"type":"object","properties":{"label":{"type":"string"},"description":{"type":"string"},"name":{"type":"string"},"defaultChecked":{"type":"boolean"}},"required":["name"],"additionalProperties":false,"description":"Individual switch toggle"},"Accordion":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/$defs/AccordionItem"}}},"required":["items"],"additionalProperties":false,"description":"Collapsible sections"},"AccordionItem":{"type":"object","properties":{"value":{"type":"string"},"trigger":{"type":"string"},"content":{"type":"array","items":{"anyOf":[{"$ref":"#/$defs/TextContent"},{"$ref":"#/$defs/MarkDownRenderer"},{"$ref":"#/$defs/CardHeader"},{"$ref":"#/$defs/Callout"},{"$ref":"#/$defs/TextCallout"},{"$ref":"#/$defs/CodeBlock"},{"$ref":"#/$defs/Image"},{"$ref":"#/$defs/ImageBlock"},{"$ref":"#/$defs/ImageGallery"},{"$ref":"#/$defs/Separator"},{"$ref":"#/$defs/HorizontalBarChart"},{"$ref":"#/$defs/RadarChart"},{"$ref":"#/$defs/PieChart"},{"$ref":"#/$defs/RadialChart"},{"$ref":"#/$defs/SingleStackedBarChart"},{"$ref":"#/$defs/ScatterChart"},{"$ref":"#/$defs/AreaChart"},{"$ref":"#/$defs/BarChart"},{"$ref":"#/$defs/LineChart"},{"$ref":"#/$defs/Table"},{"$ref":"#/$defs/TagBlock"},{"$ref":"#/$defs/Form"},{"$ref":"#/$defs/Buttons"},{"$ref":"#/$defs/IconButton"},{"$ref":"#/$defs/Steps"},{"$ref":"#/$defs/InlineHeader"},{"$ref":"#/$defs/EntityList"},{"$ref":"#/$defs/EditableTable"},{"$ref":"#/$defs/SnippetCardBlock"},{"$ref":"#/$defs/OverviewCardBlock"},{"$ref":"#/$defs/ContextCardBlock"},{"$ref":"#/$defs/CompositeCardBlock"},{"$ref":"#/$defs/VisualCardBlock"}]}}},"required":["value","trigger","content"],"additionalProperties":false,"description":"value is unique id, trigger is section title"},"Modal":{"type":"object","properties":{"title":{"type":"string"},"open":{"type":"boolean"},"children":{"type":"array","items":{"anyOf":[{"$ref":"#/$defs/TextContent"},{"$ref":"#/$defs/MarkDownRenderer"},{"$ref":"#/$defs/CardHeader"},{"$ref":"#/$defs/Callout"},{"$ref":"#/$defs/TextCallout"},{"$ref":"#/$defs/CodeBlock"},{"$ref":"#/$defs/Image"},{"$ref":"#/$defs/ImageBlock"},{"$ref":"#/$defs/ImageGallery"},{"$ref":"#/$defs/Separator"},{"$ref":"#/$defs/HorizontalBarChart"},{"$ref":"#/$defs/RadarChart"},{"$ref":"#/$defs/PieChart"},{"$ref":"#/$defs/RadialChart"},{"$ref":"#/$defs/SingleStackedBarChart"},{"$ref":"#/$defs/ScatterChart"},{"$ref":"#/$defs/AreaChart"},{"$ref":"#/$defs/BarChart"},{"$ref":"#/$defs/LineChart"},{"$ref":"#/$defs/Table"},{"$ref":"#/$defs/TagBlock"},{"$ref":"#/$defs/Form"},{"$ref":"#/$defs/Buttons"},{"$ref":"#/$defs/IconButton"},{"$ref":"#/$defs/Steps"},{"$ref":"#/$defs/InlineHeader"},{"$ref":"#/$defs/EntityList"},{"$ref":"#/$defs/EditableTable"},{"$ref":"#/$defs/SnippetCardBlock"},{"$ref":"#/$defs/OverviewCardBlock"},{"$ref":"#/$defs/ContextCardBlock"},{"$ref":"#/$defs/CompositeCardBlock"},{"$ref":"#/$defs/VisualCardBlock"}]}},"size":{"type":"string","enum":["sm","md","lg"]}},"required":["title","children"],"additionalProperties":false,"description":"Modal dialog. open is a reactive $boolean binding — set to true to open, X/Escape/backdrop auto-closes. Put Form with buttons inside children."}}}},"expected":"You are an AI assistant that responds using openui-lang, a declarative UI language. Your ENTIRE response must be valid openui-lang code — no markdown, no explanations, just openui-lang.\n\n## Syntax Rules\n\n1. Each statement is on its own line: `identifier = Expression`\n2. `root` is the entry point — every program must define `root = Stack(...)`\n3. Expressions are: strings (\"...\"), numbers, booleans (true/false), null, arrays ([...]), objects ({...}), or component calls TypeName(arg1, arg2, ...)\n4. Use references for readability: define `name = ...` on one line, then use `name` later\n5. EVERY variable (except root) MUST be referenced by at least one other variable. Unreferenced variables are silently dropped and will NOT render. Always include defined variables in their parent's children/items array.\n6. Arguments are POSITIONAL (order matters, not names). Write `SomeComp([children], \"row\", \"l\")` NOT `SomeComp([children], direction: \"row\", gap: \"l\")` — colon syntax is NOT supported and silently breaks\n7. Optional arguments can be omitted from the end\n- Strings use double quotes with backslash escaping\n\n## Component Signatures\n\nArguments marked with ? are optional. Sub-components can be inline or referenced; prefer references for better streaming.\nProps typed `ActionExpression` accept an Action([@steps...]) expression. See the Action section for available steps (@ToAssistant, @OpenUrl).\nProps marked `$binding` accept a `$variable` reference for two-way binding.\n\n### Layout\nStack(children: any[], direction?: \"row\" | \"column\", gap?: \"none\" | \"xs\" | \"s\" | \"m\" | \"l\" | \"xl\" | \"2xl\", align?: \"start\" | \"center\" | \"end\" | \"stretch\" | \"baseline\", justify?: \"start\" | \"center\" | \"end\" | \"between\" | \"around\" | \"evenly\", wrap?: boolean) — Flex container. direction: \"row\"|\"column\" (default \"column\"). gap: \"none\"|\"xs\"|\"s\"|\"m\"|\"l\"|\"xl\"|\"2xl\" (default \"m\"). align: \"start\"|\"center\"|\"end\"|\"stretch\"|\"baseline\". justify: \"start\"|\"center\"|\"end\"|\"between\"|\"around\"|\"evenly\".\nTabs(items: TabItem[]) — Tabbed container\nTabItem(value: string, trigger: string, content: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock)[]) — value is unique id, trigger is tab label, content is array of components\nAccordion(items: AccordionItem[]) — Collapsible sections\nAccordionItem(value: string, trigger: string, content: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock)[]) — value is unique id, trigger is section title\nSteps(items: StepsItem[]) — Step-by-step guide\nStepsItem(title: string, details: string) — title and details text for one step\nCarousel(children: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock)[][], variant?: \"card\" | \"sunk\") — Horizontal scrollable carousel\nSeparator(orientation?: \"horizontal\" | \"vertical\", decorative?: boolean) — Visual divider between content sections\nModal(title: string, open?: $binding, children: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock)[], size?: \"sm\" | \"md\" | \"lg\") — Modal dialog. open is a reactive $boolean binding — set to true to open, X/Escape/backdrop auto-closes. Put Form with buttons inside children.\n- For grid-like layouts, use Stack with direction \"row\" and wrap set to true.\n- Prefer justify \"start\" (or omit justify) with wrap=true for stable columns instead of uneven gutters.\n- Use nested Stacks when you need explicit rows/sections.\n- Show/hide sections: $editId != \"\" ? Card([editForm]) : null\n- Modal: Modal(\"Title\", $showModal, [content]) — $showModal is boolean, X/Escape auto-closes. Put Form with its own buttons inside children.\n- Use Tabs for alternative views (chart types, data sections) — no $variable needed\n- Shared filter across Tabs: same $days binding in Query args works across all TabItems\n\n### Content\nCard(children: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock | Tabs | Carousel | Stack)[], variant?: \"card\" | \"sunk\" | \"clear\", direction?: \"row\" | \"column\", gap?: \"none\" | \"xs\" | \"s\" | \"m\" | \"l\" | \"xl\" | \"2xl\", align?: \"start\" | \"center\" | \"end\" | \"stretch\" | \"baseline\", justify?: \"start\" | \"center\" | \"end\" | \"between\" | \"around\" | \"evenly\", wrap?: boolean) — Styled container. variant: \"card\" (default, elevated) | \"sunk\" (recessed) | \"clear\" (transparent). Always full width. Accepts all Stack flex params (default: direction \"column\"). Cards flex to share space in row/wrap layouts.\nCardHeader(title?: string, subtitle?: string) — Header with optional title and subtitle\nTextContent(text: string, size?: \"small\" | \"default\" | \"large\" | \"small-heavy\" | \"large-heavy\") — Text block. Supports markdown. Optional size: \"small\" | \"default\" | \"large\" | \"small-heavy\" | \"large-heavy\".\nMarkDownRenderer(textMarkdown: string, variant?: \"clear\" | \"card\" | \"sunk\") — Renders markdown text with optional container variant\nCallout(variant: \"info\" | \"warning\" | \"error\" | \"success\" | \"neutral\", title: string, description: string, visible?: $binding) — Callout banner. Optional visible is a reactive $boolean — auto-dismisses after 3s by setting $visible to false.\nTextCallout(variant?: \"neutral\" | \"info\" | \"warning\" | \"success\" | \"danger\", title?: string, description?: string) — Text callout with variant, title, and description\nImage(alt: string, src?: string) — Image with alt text and optional URL\nImageBlock(src: string, alt?: string) — Image block with loading state\nImageGallery(images: {src: string, alt?: string, details?: string}[]) — Gallery grid of images with modal preview\nCodeBlock(language: string, codeString: string) — Syntax-highlighted code block\nInlineHeader(heading: string, description?: string) — Compact section heading with an optional one-line description, for use inside cards.\n- InlineHeader is a compact heading + description pair for labelling a block (lighter than CardHeader).\n- Use Cards to group related KPIs or sections. Stack with direction \"row\" for side-by-side layouts.\n- Success toast: Callout(\"success\", \"Saved\", \"Done.\", $showSuccess) — use @Set($showSuccess, true) in save action, auto-dismisses after 3s. For errors: result.status == \"error\" ? Callout(\"error\", \"Failed\", result.error) : null\n- KPI card: Card([TextContent(\"Label\", \"small\"), TextContent(\"\" + @Count(@Filter(data.rows, \"field\", \"==\", \"value\")), \"large-heavy\")])\n\n### Tables\nTable(columns: Col[]) — Data table — column-oriented. Each Col holds its own data array.\nCol(label: string, data: any, type?: \"string\" | \"number\" | \"action\") — Column definition — holds label + data array\nEditableTable(name?: string, columns?: {type: \"text\" | \"number\" | \"date-single\" | \"select\" | \"url\", key?: string, header?: string, width?: number, options?: {value: string, label: string}[]}[], data?: {id: string, values: (string | number)[]}[]) — Spreadsheet-like table whose cells the user can edit inline (text, number, url, date, select columns); edits are saved back as a form field\n- EditableTable lets the user edit cells inline. Give it a unique name, columns of { type, key, header } with type one of text | number | date-single | select | url (select also needs options: [{ value, label }]).\n- data is an array of { id, values } rows where values are ordered positionally to match columns. Edited data is submitted when the user clicks Save Changes.\n- Table is COLUMN-oriented: Table([Col(\"Label\", dataArray), Col(\"Count\", countArray, \"number\")]). Use array pluck for data: data.rows.fieldName\n- Col data can be component arrays for styled cells: Col(\"Status\", @Each(data.rows, \"item\", Tag(item.status, null, \"sm\", item.status == \"open\" ? \"success\" : \"danger\")))\n- Row actions: Col(\"Actions\", @Each(data.rows, \"t\", Button(\"Edit\", Action([@Set($showEdit, true), @Set($editId, t.id)]))))\n- Sortable: sorted = @Sort(data.rows, $sortField, \"desc\"). Bind $sortField to Select. Use sorted.fieldName for Col data\n- Searchable: filtered = @Filter(data.rows, \"title\", \"contains\", $search). Bind $search to Input\n- Chain sort + filter: filtered = @Filter(...) then sorted = @Sort(filtered, ...) — use sorted for both Table and Charts\n- Empty state: @Count(data.rows) > 0 ? Table([...]) : TextContent(\"No data yet\")\n\n### Charts (2D)\nBarChart(labels: string[], series: Series[], variant?: \"grouped\" | \"stacked\", xLabel?: string, yLabel?: string, height?: number) — Vertical bars; use for comparing values across categories with one or more series\nLineChart(labels: string[], series: Series[], variant?: \"linear\" | \"natural\" | \"step\", xLabel?: string, yLabel?: string, height?: number) — Lines over categories; use for trends and continuous data over time\nAreaChart(labels: string[], series: Series[], variant?: \"linear\" | \"natural\" | \"step\", xLabel?: string, yLabel?: string, height?: number) — Filled area under lines; use for cumulative totals or volume trends over time\nRadarChart(labels: string[], series: Series[]) — Spider/web chart; use for comparing multiple variables across one or more entities\nHorizontalBarChart(labels: string[], series: Series[], variant?: \"grouped\" | \"stacked\", xLabel?: string, yLabel?: string) — Horizontal bars; prefer when category labels are long or for ranked lists\nSeries(category: string, values: number[]) — One data series\n- Charts accept column arrays: LineChart(labels, [Series(\"Name\", values)]). Use array pluck: LineChart(data.rows.day, [Series(\"Views\", data.rows.views)])\n- Use Cards to wrap charts with CardHeader for titled sections\n- Chart + Table from same source: use @Sort or @Filter result for both LineChart and Table Col data\n- Multiple chart views: use Tabs — Tabs([TabItem(\"line\", \"Line\", [LineChart(...)]), TabItem(\"bar\", \"Bar\", [BarChart(...)])])\n\n### Charts (1D)\nPieChart(labels: string[], values: number[], variant?: \"pie\" | \"donut\", appearance?: \"circular\" | \"semiCircular\") — Circular slices; use plucked arrays: PieChart(data.categories, data.values)\nRadialChart(labels: string[], values: number[]) — Radial bars; use plucked arrays: RadialChart(data.categories, data.values)\nSingleStackedBarChart(labels: string[], values: number[]) — Single horizontal stacked bar; use plucked arrays: SingleStackedBarChart(data.categories, data.values)\nSlice(category: string, value: number) — One slice with label and numeric value\n- PieChart and BarChart need NUMBERS, not objects. For list data, use @Count(@Filter(...)) to aggregate:\n- PieChart from list: `PieChart([\"Low\", \"Med\", \"High\"], [@Count(@Filter(data.rows, \"priority\", \"==\", \"low\")), @Count(@Filter(data.rows, \"priority\", \"==\", \"medium\")), @Count(@Filter(data.rows, \"priority\", \"==\", \"high\"))], \"donut\")`\n- KPI from count: `TextContent(\"\" + @Count(@Filter(data.rows, \"status\", \"==\", \"open\")), \"large-heavy\")`\n\n### Charts (Scatter)\nScatterChart(datasets: ScatterSeries[], xLabel?: string, yLabel?: string) — X/Y scatter plot; use for correlations, distributions, and clustering\nScatterSeries(name: string, points: Point[]) — Named dataset\nPoint(x: number, y: number, z?: number) — Data point with numeric coordinates\n\n### Forms\nForm(name: string, buttons: Buttons, fields?: FormControl[]) — Form container with fields and explicit action buttons\nFormControl(label: string, input: Input | TextArea | Select | DatePicker | Slider | CheckBoxGroup | RadioGroup | Chips | OptionCards, hint?: string) — Field with label, input component, and optional hint text\nLabel(text: string) — Text label\nInput(name: string, placeholder?: string, type?: \"text\" | \"email\" | \"password\" | \"number\" | \"url\", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)\nTextArea(name: string, placeholder?: string, rows?: number, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)\nSelect(name: string, items: SelectItem[], placeholder?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding, size?: \"small\" | \"medium\" | \"large\")\nSelectItem(value: string, label: string) — Option for Select\nDatePicker(name: string, mode?: \"single\" | \"range\", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)\nSlider(name: string, variant: \"continuous\" | \"discrete\", min: number, max: number, step?: number, defaultValue?: number[], label?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding) — Numeric slider input; supports continuous and discrete (stepped) variants\nCheckBoxGroup(name: string, items: CheckBoxItem[], rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding>)\nCheckBoxItem(label: string, description: string, name: string, defaultChecked?: boolean)\nRadioGroup(name: string, items: RadioItem[], defaultValue?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)\nRadioItem(label: string, description: string, value: string)\nSwitchGroup(name: string, items: SwitchItem[], variant?: \"clear\" | \"card\" | \"sunk\", value?: $binding>) — Group of switch toggles\nSwitchItem(label?: string, description?: string, name: string, defaultChecked?: boolean) — Individual switch toggle\nChips(name: string, type?: \"single\" | \"multiple\", items?: ChipItem[], rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, defaultValue?: string | string[]) — A form field of compact selectable chips for choosing one or many short options; the selection is stored under `name`.\nChipItem(value: string, label: string, icon?: Icon, disabled?: boolean) — A single selectable chip inside a Chips group, with a value, label and optional icon.\nOptionCards(name: string, type?: \"single\" | \"multiple\", items?: OptionCard[], rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, defaultValue?: string | string[]) — A form field of selectable cards (title, optional subtitle, optional icon or image) laid out in a responsive grid for choosing one or many options; the selection is stored under `name`.\nOptionCard(value: string, title: string, subtitle?: string, topContent?: Icon | Image, disabled?: boolean) — A single selectable card inside an OptionCards group, with a value, title, optional subtitle and an optional Icon or Image on top.\n- Chips: compact single/multiple selection pills. Use ChipItem references for each option.\n- OptionCards: larger selectable cards with title, subtitle and an optional Icon or Image on top. Use OptionCard references for each option.\n- For Form fields, define EACH FormControl as its own reference — do NOT inline all controls in one array. This allows progressive field-by-field streaming.\n- NEVER nest Form inside Form — each Form should be a standalone container.\n- Form requires explicit buttons. Always pass a Buttons(...) reference as the third Form argument.\n- rules is an optional object: {required: true, email: true, minLength: 8, maxLength: 100}\n- Available rules: required, email, min, max, minLength, maxLength, pattern, url, numeric\n- The renderer shows error messages automatically — do NOT generate error text in the UI\n- Conditional fields: $country == \"US\" ? stateField : $country == \"UK\" ? postcodeField : addressField\n- Edit form in Modal: Modal(\"Edit\", $showEdit, [Form(\"edit\", Buttons([saveBtn, cancelBtn]), [fields...])]). Save button should include @Set($showEdit, false) to close modal.\n\n### Buttons\nButton(label: string, action?: ActionExpression, variant?: \"primary\" | \"secondary\" | \"tertiary\", type?: \"normal\" | \"destructive\", size?: \"extra-small\" | \"small\" | \"medium\" | \"large\") — Clickable button\nButtons(buttons: Button[], direction?: \"row\" | \"column\") — Group of Button components. direction: \"row\" (default) | \"column\".\nIconButton(name: string, icon: Icon, action?: ActionExpression, variant?: \"primary\" | \"secondary\" | \"tertiary\", size?: \"extra-small\" | \"small\" | \"medium\" | \"large\", shape?: \"square\" | \"circle\") — Icon-only button. name is the accessible label and the action label; icon is an Icon; action fires on click.\n- Icon renders a lucide icon by kebab-case name; it is also used as the icon of IconButton, IconText and OptionCard.\n- Toggle in @Each: @Each(rows, \"t\", Button(t.status == \"open\" ? \"Close\" : \"Reopen\", Action([...])))\n\n### Data Display\nTagBlock(tags: string[], size?: \"sm\" | \"md\" | \"lg\") — tags is an array of strings; optional size sm | md | lg\nTag(text: string, icon?: Icon, size?: \"sm\" | \"md\" | \"lg\", variant?: \"neutral\" | \"info\" | \"success\" | \"warning\" | \"danger\") — Styled tag/badge with optional Icon and variant\nIcon(name: string, category?: string) — A lucide icon by kebab-case name (e.g. 'circle-check'). Optional category picks a topical fallback when the name doesn't resolve.\nEntityList(rows?: {left: string, right: string, rightVariant?: \"text\" | \"number\"}[], size?: \"small\" | \"default\", header?: {left: string, right: string, rightVariant?: \"text\" | \"number\"}, footer?: {left: string, right: string, rightVariant?: \"text\" | \"number\"}) — Two-column key/value rows (left label, right value). size 'default' supports optional header and footer rows; rightVariant 'number' uses tabular numbers.\nListBlock(items: ListItem[], variant?: \"number\" | \"image\", size?: \"default\" | \"small\") — A list of items with number or image indicators. Each item can optionally have an action. size small renders a compact list.\nListItem(title: string, subtitle?: string, image?: {src: string, alt: string}, actionLabel?: string, action?: ActionExpression) — Item in a ListBlock — displays a title with an optional subtitle and image. When action is provided, the item becomes clickable.\n- Color-mapped Tag: Tag(value, null, \"sm\", value == \"high\" ? \"danger\" : value == \"medium\" ? \"warning\" : \"neutral\")\n- EntityList is a compact two-column list of { left, right } rows (e.g. name / value). size='default' also supports a header and footer row; size='small' does not.\n- ListBlock is a numbered or image list of ListItem references. An action on ListItem is optional.\n\n### Cards\nSnippetCardBlock(items: SnippetCardItem[], layout?: \"grid\", responsive?: boolean, action?: ActionExpression, gap?: number | string) — A responsive grid of compact label/value cards (2 per row) for showing several short facts side by side; optionally clickable with a shared action.\nSnippetCardItem(id?: string, lhs: IconText | ImageText, rhs?: Text | BoldText) — One row-style snippet card: a label on the left (IconText or ImageText) and an optional value on the right (Text or BoldText).\nOverviewCardBlock(items: OverviewCardItem[], layout?: \"grid\" | \"carousel\", responsive?: boolean, action?: ActionExpression, gap?: number | string) — A grid or horizontal carousel of compact overview cards, each with a heading (icon/image/text) on top and an inline metric below; optionally clickable with a shared action.\nOverviewCardItem(id?: string, top: IconText | ImageText | Text, bottom?: MetricIndicatorInline) — One overview card: a heading slot at the top (IconText, ImageText or Text) and an optional MetricIndicatorInline at the bottom.\nContextCardBlock(items: ContextCardItem[], layout?: \"grid\" | \"carousel\", responsive?: boolean, action?: ActionExpression, gap?: number | string) — A grid or carousel of compact tinted context cards (title or tag plus a short bold body); an optional action makes every card clickable.\nContextCardItem(id?: string, title: string | Tag, body?: string, bgColor?: \"gray\", bgImageSrc?: string, bgImageAlt?: string) — A single card inside a ContextCardBlock: a title (plain string or Tag), an optional markdown body, and an optional gray tint or background image.\nCompositeCardBlock(items: CompositeCardItem[], layout?: \"grid\" | \"carousel\", responsive?: boolean, action?: ActionExpression, gap?: number | string) — A two-per-row grid or carousel of rich cards, each with an optional header, stacked body content (text, metrics, charts, lists, tags) and a price/button footer; an optional action makes every card clickable.\nCompositeCardItem(id?: string, header?: IconText | ImageText | ImageTextLarge | Text | Image, body?: (Text | BoldText | MetricIndicatorInline | IconText | Image | AreaChart | BarChart | LineChart | ListBlock | TagBlock | EntityList)[], footer?: {price?: BoldText | MetricIndicatorWithStrikethrough, button?: Button}) — A single card inside a CompositeCardBlock: an optional header (icon/image/text), a stack of body elements (text, metrics, charts, lists, tags), and an optional price/button footer.\nVisualCardBlock(items: VisualCardItem[], layout?: \"grid\" | \"carousel\", responsive?: boolean, action?: ActionExpression, gap?: number | string) — A grid or carousel of photo-first cards: a full-bleed background image with a tag on top and a bold text panel at the bottom; an optional action makes every card clickable.\nVisualCardItem(body: BoldText, id?: string, bgImageSrc?: string, tag?: Tag, bgImageAlt?: string) — A single photo-first card inside a VisualCardBlock: a BoldText body panel, an optional Tag, and a background image (bgImageSrc must be a real URL; bgImageAlt is its alt text).\nText(variant?: \"text\" | \"number\", value: string, subtext?: string, subtextVariant?: \"text\" | \"number\" | \"metric\", size?: \"xs\" | \"sm\" | \"md\" | \"lg\") — Plain text line with optional subtext. variant 'number' uses tabular number styling; subtextVariant 'metric' colors a leading +/- subtext green/red.\nBoldText(variant?: \"text\" | \"number\", value: string, subtext?: string, subtextVariant?: \"text\" | \"number\" | \"metric\", size?: \"xs\" | \"sm\" | \"md\" | \"lg\") — Emphasized (bold) text line with optional subtext. variant 'number' uses tabular number styling; subtextVariant 'metric' colors a leading +/- subtext green/red.\nIconText(icon: Icon, iconVariant?: \"neutral\" | \"info\" | \"success\" | \"warning\" | \"danger\" | \"inverted\" | \"filled\" | \"soft\", iconSize?: \"xs\" | \"s\" | \"m\" | \"l\" | \"xl\" | \"sm\" | \"md\" | \"lg\", title: string, subtitle?: string, bold?: boolean, layout?: \"horizontal\" | \"vertical\") — An icon badge with a title and optional subtitle, laid out horizontally or vertically. iconVariant sets the badge color.\nImageText(src: string, alt?: string, title: string, subtitle?: string, bold?: boolean, layout?: \"horizontal\" | \"vertical\", imageSize?: number) — A small square image (thumbnail/avatar) with a title and optional subtitle. src must be a real image URL.\nImageTextLarge(src: string, alt?: string, title: string, subtitle?: string, bold?: boolean) — A full-width banner image above a bold title and optional subtitle. src must be a real image URL.\nMetricIndicatorInline(value: string, subtext?: string, trend?: {direction: \"up\" | \"down\", value: number}) — Headline metric value with an optional +/- percentage trend and subtext, all on one line.\nMetricIndicatorWithStrikethrough(value: string, subtext?: string, previousValue?: string, trend?: {direction: \"up\" | \"down\", value: number}) — Headline metric value with an optional struck-through previousValue, a +/- percentage trend, and subtext below.\n- Card blocks lay out 2+ items in a responsive grid (or carousel where supported). Every item in a block must have the same structure.\n- SnippetCardItem: small card with lhs (IconText | ImageText) and optional rhs (Text | BoldText) — good for key/value facts.\n- OverviewCardItem: small card with top (IconText | ImageText | Text) and optional bottom MetricIndicatorInline — good for KPIs.\n- ContextCardItem: medium card with a title (string or Tag), body text and optional background image — good for summaries.\n- CompositeCardItem: rich card with header, body array (Text, BoldText, MetricIndicatorInline, IconText, Image, charts, ListBlock, TagBlock, EntityList) and footer (price + Button) — good for products/offers.\n- VisualCardItem: image-first card with a BoldText body and optional Tag.\n- Text / BoldText / IconText / ImageText / ImageTextLarge / MetricIndicator* are the inline building blocks used INSIDE card items; do not place them directly in the root Card.\n\n## Action — Button Behavior\n\nAction([@steps...]) wires button clicks to operations. Steps are @-prefixed built-in actions. Steps execute in order.\nButtons without an explicit Action prop automatically send their label to the assistant (equivalent to Action([@ToAssistant(label)])).\n\nAvailable steps:\n- @ToAssistant(\"message\") — Send a message to the assistant (for conversational buttons like \"Tell me more\", \"Explain this\")\n- @OpenUrl(\"https://...\") — Navigate to a URL\n\nExample — simple nav:\n```\nviewBtn = Button(\"View\", Action([@OpenUrl(\"https://example.com\")]))\n```\n\n- Action can be assigned to a variable or inlined: Button(\"Go\", onSubmit) and Button(\"Go\", Action([...])) both work\n\n## Hoisting & Streaming (CRITICAL)\n\nopenui-lang supports hoisting: a reference can be used BEFORE it is defined. The parser resolves all references after the full input is parsed.\n\nDuring streaming, the output is re-parsed on every chunk. Undefined references are temporarily unresolved and appear once their definitions stream in. This creates a progressive top-down reveal — structure first, then data fills in.\n\n**Recommended statement order for optimal streaming:**\n1. `root = Stack(...)` — UI shell appears immediately\n2. Component definitions — fill in as they stream\n3. Data values — leaf content last\n\nAlways write the root = Stack(...) statement first so the UI shell appears immediately, even before child data has streamed in.\n## Important Rules\n- Choose components that best represent the content (tables for comparisons, charts for trends, forms for input, etc.)\n\n## Final Verification\nBefore finishing, walk your output and verify:\n1. root = Stack(...) is the FIRST line (for optimal streaming).\n2. Every referenced name is defined. Every defined name (other than root) is reachable from root."}, +{"name":"openui-options","spec":{"root":"Stack","components":{"Card":{"signature":"Card(children: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock | Tabs | Carousel | Stack)[], variant?: \"card\" | \"sunk\" | \"clear\", direction?: \"row\" | \"column\", gap?: \"none\" | \"xs\" | \"s\" | \"m\" | \"l\" | \"xl\" | \"2xl\", align?: \"start\" | \"center\" | \"end\" | \"stretch\" | \"baseline\", justify?: \"start\" | \"center\" | \"end\" | \"between\" | \"around\" | \"evenly\", wrap?: boolean)","description":"Styled container. variant: \"card\" (default, elevated) | \"sunk\" (recessed) | \"clear\" (transparent). Always full width. Accepts all Stack flex params (default: direction \"column\"). Cards flex to share space in row/wrap layouts."},"CardHeader":{"signature":"CardHeader(title?: string, subtitle?: string)","description":"Header with optional title and subtitle"},"TextContent":{"signature":"TextContent(text: string, size?: \"small\" | \"default\" | \"large\" | \"small-heavy\" | \"large-heavy\")","description":"Text block. Supports markdown. Optional size: \"small\" | \"default\" | \"large\" | \"small-heavy\" | \"large-heavy\"."},"MarkDownRenderer":{"signature":"MarkDownRenderer(textMarkdown: string, variant?: \"clear\" | \"card\" | \"sunk\")","description":"Renders markdown text with optional container variant"},"Callout":{"signature":"Callout(variant: \"info\" | \"warning\" | \"error\" | \"success\" | \"neutral\", title: string, description: string, visible?: $binding)","description":"Callout banner. Optional visible is a reactive $boolean — auto-dismisses after 3s by setting $visible to false."},"TextCallout":{"signature":"TextCallout(variant?: \"neutral\" | \"info\" | \"warning\" | \"success\" | \"danger\", title?: string, description?: string)","description":"Text callout with variant, title, and description"},"Image":{"signature":"Image(alt: string, src?: string)","description":"Image with alt text and optional URL"},"ImageBlock":{"signature":"ImageBlock(src: string, alt?: string)","description":"Image block with loading state"},"ImageGallery":{"signature":"ImageGallery(images: {src: string, alt?: string, details?: string}[])","description":"Gallery grid of images with modal preview"},"CodeBlock":{"signature":"CodeBlock(language: string, codeString: string)","description":"Syntax-highlighted code block"},"InlineHeader":{"signature":"InlineHeader(heading: string, description?: string)","description":"Compact section heading with an optional one-line description, for use inside cards."},"Table":{"signature":"Table(columns: Col[])","description":"Data table — column-oriented. Each Col holds its own data array."},"Col":{"signature":"Col(label: string, data: any, type?: \"string\" | \"number\" | \"action\")","description":"Column definition — holds label + data array"},"EditableTable":{"signature":"EditableTable(name?: string, columns?: {type: \"text\" | \"number\" | \"date-single\" | \"select\" | \"url\", key?: string, header?: string, width?: number, options?: {value: string, label: string}[]}[], data?: {id: string, values: (string | number)[]}[])","description":"Spreadsheet-like table whose cells the user can edit inline (text, number, url, date, select columns); edits are saved back as a form field"},"BarChart":{"signature":"BarChart(labels: string[], series: Series[], variant?: \"grouped\" | \"stacked\", xLabel?: string, yLabel?: string, height?: number)","description":"Vertical bars; use for comparing values across categories with one or more series"},"LineChart":{"signature":"LineChart(labels: string[], series: Series[], variant?: \"linear\" | \"natural\" | \"step\", xLabel?: string, yLabel?: string, height?: number)","description":"Lines over categories; use for trends and continuous data over time"},"AreaChart":{"signature":"AreaChart(labels: string[], series: Series[], variant?: \"linear\" | \"natural\" | \"step\", xLabel?: string, yLabel?: string, height?: number)","description":"Filled area under lines; use for cumulative totals or volume trends over time"},"RadarChart":{"signature":"RadarChart(labels: string[], series: Series[])","description":"Spider/web chart; use for comparing multiple variables across one or more entities"},"HorizontalBarChart":{"signature":"HorizontalBarChart(labels: string[], series: Series[], variant?: \"grouped\" | \"stacked\", xLabel?: string, yLabel?: string)","description":"Horizontal bars; prefer when category labels are long or for ranked lists"},"Series":{"signature":"Series(category: string, values: number[])","description":"One data series"},"PieChart":{"signature":"PieChart(labels: string[], values: number[], variant?: \"pie\" | \"donut\", appearance?: \"circular\" | \"semiCircular\")","description":"Circular slices; use plucked arrays: PieChart(data.categories, data.values)"},"RadialChart":{"signature":"RadialChart(labels: string[], values: number[])","description":"Radial bars; use plucked arrays: RadialChart(data.categories, data.values)"},"SingleStackedBarChart":{"signature":"SingleStackedBarChart(labels: string[], values: number[])","description":"Single horizontal stacked bar; use plucked arrays: SingleStackedBarChart(data.categories, data.values)"},"Slice":{"signature":"Slice(category: string, value: number)","description":"One slice with label and numeric value"},"ScatterChart":{"signature":"ScatterChart(datasets: ScatterSeries[], xLabel?: string, yLabel?: string)","description":"X/Y scatter plot; use for correlations, distributions, and clustering"},"ScatterSeries":{"signature":"ScatterSeries(name: string, points: Point[])","description":"Named dataset"},"Point":{"signature":"Point(x: number, y: number, z?: number)","description":"Data point with numeric coordinates"},"Form":{"signature":"Form(name: string, buttons: Buttons, fields?: FormControl[])","description":"Form container with fields and explicit action buttons"},"FormControl":{"signature":"FormControl(label: string, input: Input | TextArea | Select | DatePicker | Slider | CheckBoxGroup | RadioGroup | Chips | OptionCards, hint?: string)","description":"Field with label, input component, and optional hint text"},"Label":{"signature":"Label(text: string)","description":"Text label"},"Input":{"signature":"Input(name: string, placeholder?: string, type?: \"text\" | \"email\" | \"password\" | \"number\" | \"url\", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)","description":""},"TextArea":{"signature":"TextArea(name: string, placeholder?: string, rows?: number, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)","description":""},"Select":{"signature":"Select(name: string, items: SelectItem[], placeholder?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding, size?: \"small\" | \"medium\" | \"large\")","description":""},"SelectItem":{"signature":"SelectItem(value: string, label: string)","description":"Option for Select"},"DatePicker":{"signature":"DatePicker(name: string, mode?: \"single\" | \"range\", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)","description":""},"Slider":{"signature":"Slider(name: string, variant: \"continuous\" | \"discrete\", min: number, max: number, step?: number, defaultValue?: number[], label?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)","description":"Numeric slider input; supports continuous and discrete (stepped) variants"},"CheckBoxGroup":{"signature":"CheckBoxGroup(name: string, items: CheckBoxItem[], rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding>)","description":""},"CheckBoxItem":{"signature":"CheckBoxItem(label: string, description: string, name: string, defaultChecked?: boolean)","description":""},"RadioGroup":{"signature":"RadioGroup(name: string, items: RadioItem[], defaultValue?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)","description":""},"RadioItem":{"signature":"RadioItem(label: string, description: string, value: string)","description":""},"SwitchGroup":{"signature":"SwitchGroup(name: string, items: SwitchItem[], variant?: \"clear\" | \"card\" | \"sunk\", value?: $binding>)","description":"Group of switch toggles"},"SwitchItem":{"signature":"SwitchItem(label?: string, description?: string, name: string, defaultChecked?: boolean)","description":"Individual switch toggle"},"ChipItem":{"signature":"ChipItem(value: string, label: string, icon?: Icon, disabled?: boolean)","description":"A single selectable chip inside a Chips group, with a value, label and optional icon."},"Chips":{"signature":"Chips(name: string, type?: \"single\" | \"multiple\", items?: ChipItem[], rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, defaultValue?: string | string[])","description":"A form field of compact selectable chips for choosing one or many short options; the selection is stored under `name`."},"OptionCard":{"signature":"OptionCard(value: string, title: string, subtitle?: string, topContent?: Icon | Image, disabled?: boolean)","description":"A single selectable card inside an OptionCards group, with a value, title, optional subtitle and an optional Icon or Image on top."},"OptionCards":{"signature":"OptionCards(name: string, type?: \"single\" | \"multiple\", items?: OptionCard[], rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, defaultValue?: string | string[])","description":"A form field of selectable cards (title, optional subtitle, optional icon or image) laid out in a responsive grid for choosing one or many options; the selection is stored under `name`."},"Button":{"signature":"Button(label: string, action?: ActionExpression, variant?: \"primary\" | \"secondary\" | \"tertiary\", type?: \"normal\" | \"destructive\", size?: \"extra-small\" | \"small\" | \"medium\" | \"large\")","description":"Clickable button"},"Buttons":{"signature":"Buttons(buttons: Button[], direction?: \"row\" | \"column\")","description":"Group of Button components. direction: \"row\" (default) | \"column\"."},"IconButton":{"signature":"IconButton(name: string, icon: Icon, action?: ActionExpression, variant?: \"primary\" | \"secondary\" | \"tertiary\", size?: \"extra-small\" | \"small\" | \"medium\" | \"large\", shape?: \"square\" | \"circle\")","description":"Icon-only button. name is the accessible label and the action label; icon is an Icon; action fires on click."},"Stack":{"signature":"Stack(children: any[], direction?: \"row\" | \"column\", gap?: \"none\" | \"xs\" | \"s\" | \"m\" | \"l\" | \"xl\" | \"2xl\", align?: \"start\" | \"center\" | \"end\" | \"stretch\" | \"baseline\", justify?: \"start\" | \"center\" | \"end\" | \"between\" | \"around\" | \"evenly\", wrap?: boolean)","description":"Flex container. direction: \"row\"|\"column\" (default \"column\"). gap: \"none\"|\"xs\"|\"s\"|\"m\"|\"l\"|\"xl\"|\"2xl\" (default \"m\"). align: \"start\"|\"center\"|\"end\"|\"stretch\"|\"baseline\". justify: \"start\"|\"center\"|\"end\"|\"between\"|\"around\"|\"evenly\"."},"Tabs":{"signature":"Tabs(items: TabItem[])","description":"Tabbed container"},"TabItem":{"signature":"TabItem(value: string, trigger: string, content: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock)[])","description":"value is unique id, trigger is tab label, content is array of components"},"Accordion":{"signature":"Accordion(items: AccordionItem[])","description":"Collapsible sections"},"AccordionItem":{"signature":"AccordionItem(value: string, trigger: string, content: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock)[])","description":"value is unique id, trigger is section title"},"Steps":{"signature":"Steps(items: StepsItem[])","description":"Step-by-step guide"},"StepsItem":{"signature":"StepsItem(title: string, details: string)","description":"title and details text for one step"},"Carousel":{"signature":"Carousel(children: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock)[][], variant?: \"card\" | \"sunk\")","description":"Horizontal scrollable carousel"},"Separator":{"signature":"Separator(orientation?: \"horizontal\" | \"vertical\", decorative?: boolean)","description":"Visual divider between content sections"},"TagBlock":{"signature":"TagBlock(tags: string[], size?: \"sm\" | \"md\" | \"lg\")","description":"tags is an array of strings; optional size sm | md | lg"},"Tag":{"signature":"Tag(text: string, icon?: Icon, size?: \"sm\" | \"md\" | \"lg\", variant?: \"neutral\" | \"info\" | \"success\" | \"warning\" | \"danger\")","description":"Styled tag/badge with optional Icon and variant"},"Icon":{"signature":"Icon(name: string, category?: string)","description":"A lucide icon by kebab-case name (e.g. 'circle-check'). Optional category picks a topical fallback when the name doesn't resolve."},"EntityList":{"signature":"EntityList(rows?: {left: string, right: string, rightVariant?: \"text\" | \"number\"}[], size?: \"small\" | \"default\", header?: {left: string, right: string, rightVariant?: \"text\" | \"number\"}, footer?: {left: string, right: string, rightVariant?: \"text\" | \"number\"})","description":"Two-column key/value rows (left label, right value). size 'default' supports optional header and footer rows; rightVariant 'number' uses tabular numbers."},"ListBlock":{"signature":"ListBlock(items: ListItem[], variant?: \"number\" | \"image\", size?: \"default\" | \"small\")","description":"A list of items with number or image indicators. Each item can optionally have an action. size small renders a compact list."},"ListItem":{"signature":"ListItem(title: string, subtitle?: string, image?: {src: string, alt: string}, actionLabel?: string, action?: ActionExpression)","description":"Item in a ListBlock — displays a title with an optional subtitle and image. When action is provided, the item becomes clickable."},"Text":{"signature":"Text(variant?: \"text\" | \"number\", value: string, subtext?: string, subtextVariant?: \"text\" | \"number\" | \"metric\", size?: \"xs\" | \"sm\" | \"md\" | \"lg\")","description":"Plain text line with optional subtext. variant 'number' uses tabular number styling; subtextVariant 'metric' colors a leading +/- subtext green/red."},"BoldText":{"signature":"BoldText(variant?: \"text\" | \"number\", value: string, subtext?: string, subtextVariant?: \"text\" | \"number\" | \"metric\", size?: \"xs\" | \"sm\" | \"md\" | \"lg\")","description":"Emphasized (bold) text line with optional subtext. variant 'number' uses tabular number styling; subtextVariant 'metric' colors a leading +/- subtext green/red."},"IconText":{"signature":"IconText(icon: Icon, iconVariant?: \"neutral\" | \"info\" | \"success\" | \"warning\" | \"danger\" | \"inverted\" | \"filled\" | \"soft\", iconSize?: \"xs\" | \"s\" | \"m\" | \"l\" | \"xl\" | \"sm\" | \"md\" | \"lg\", title: string, subtitle?: string, bold?: boolean, layout?: \"horizontal\" | \"vertical\")","description":"An icon badge with a title and optional subtitle, laid out horizontally or vertically. iconVariant sets the badge color."},"ImageText":{"signature":"ImageText(src: string, alt?: string, title: string, subtitle?: string, bold?: boolean, layout?: \"horizontal\" | \"vertical\", imageSize?: number)","description":"A small square image (thumbnail/avatar) with a title and optional subtitle. src must be a real image URL."},"ImageTextLarge":{"signature":"ImageTextLarge(src: string, alt?: string, title: string, subtitle?: string, bold?: boolean)","description":"A full-width banner image above a bold title and optional subtitle. src must be a real image URL."},"MetricIndicatorInline":{"signature":"MetricIndicatorInline(value: string, subtext?: string, trend?: {direction: \"up\" | \"down\", value: number})","description":"Headline metric value with an optional +/- percentage trend and subtext, all on one line."},"MetricIndicatorWithStrikethrough":{"signature":"MetricIndicatorWithStrikethrough(value: string, subtext?: string, previousValue?: string, trend?: {direction: \"up\" | \"down\", value: number})","description":"Headline metric value with an optional struck-through previousValue, a +/- percentage trend, and subtext below."},"SnippetCardItem":{"signature":"SnippetCardItem(id?: string, lhs: IconText | ImageText, rhs?: Text | BoldText)","description":"One row-style snippet card: a label on the left (IconText or ImageText) and an optional value on the right (Text or BoldText)."},"SnippetCardBlock":{"signature":"SnippetCardBlock(items: SnippetCardItem[], layout?: \"grid\", responsive?: boolean, action?: ActionExpression, gap?: number | string)","description":"A responsive grid of compact label/value cards (2 per row) for showing several short facts side by side; optionally clickable with a shared action."},"OverviewCardItem":{"signature":"OverviewCardItem(id?: string, top: IconText | ImageText | Text, bottom?: MetricIndicatorInline)","description":"One overview card: a heading slot at the top (IconText, ImageText or Text) and an optional MetricIndicatorInline at the bottom."},"OverviewCardBlock":{"signature":"OverviewCardBlock(items: OverviewCardItem[], layout?: \"grid\" | \"carousel\", responsive?: boolean, action?: ActionExpression, gap?: number | string)","description":"A grid or horizontal carousel of compact overview cards, each with a heading (icon/image/text) on top and an inline metric below; optionally clickable with a shared action."},"ContextCardItem":{"signature":"ContextCardItem(id?: string, title: string | Tag, body?: string, bgColor?: \"gray\", bgImageSrc?: string, bgImageAlt?: string)","description":"A single card inside a ContextCardBlock: a title (plain string or Tag), an optional markdown body, and an optional gray tint or background image."},"ContextCardBlock":{"signature":"ContextCardBlock(items: ContextCardItem[], layout?: \"grid\" | \"carousel\", responsive?: boolean, action?: ActionExpression, gap?: number | string)","description":"A grid or carousel of compact tinted context cards (title or tag plus a short bold body); an optional action makes every card clickable."},"CompositeCardItem":{"signature":"CompositeCardItem(id?: string, header?: IconText | ImageText | ImageTextLarge | Text | Image, body?: (Text | BoldText | MetricIndicatorInline | IconText | Image | AreaChart | BarChart | LineChart | ListBlock | TagBlock | EntityList)[], footer?: {price?: BoldText | MetricIndicatorWithStrikethrough, button?: Button})","description":"A single card inside a CompositeCardBlock: an optional header (icon/image/text), a stack of body elements (text, metrics, charts, lists, tags), and an optional price/button footer."},"CompositeCardBlock":{"signature":"CompositeCardBlock(items: CompositeCardItem[], layout?: \"grid\" | \"carousel\", responsive?: boolean, action?: ActionExpression, gap?: number | string)","description":"A two-per-row grid or carousel of rich cards, each with an optional header, stacked body content (text, metrics, charts, lists, tags) and a price/button footer; an optional action makes every card clickable."},"VisualCardItem":{"signature":"VisualCardItem(body: BoldText, id?: string, bgImageSrc?: string, tag?: Tag, bgImageAlt?: string)","description":"A single photo-first card inside a VisualCardBlock: a BoldText body panel, an optional Tag, and a background image (bgImageSrc must be a real URL; bgImageAlt is its alt text)."},"VisualCardBlock":{"signature":"VisualCardBlock(items: VisualCardItem[], layout?: \"grid\" | \"carousel\", responsive?: boolean, action?: ActionExpression, gap?: number | string)","description":"A grid or carousel of photo-first cards: a full-bleed background image with a tag on top and a bold text panel at the bottom; an optional action makes every card clickable."},"Modal":{"signature":"Modal(title: string, open?: $binding, children: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock)[], size?: \"sm\" | \"md\" | \"lg\")","description":"Modal dialog. open is a reactive $boolean binding — set to true to open, X/Escape/backdrop auto-closes. Put Form with buttons inside children."}},"componentGroups":[{"name":"Layout","components":["Stack","Tabs","TabItem","Accordion","AccordionItem","Steps","StepsItem","Carousel","Separator","Modal"],"notes":["- For grid-like layouts, use Stack with direction \"row\" and wrap set to true.","- Prefer justify \"start\" (or omit justify) with wrap=true for stable columns instead of uneven gutters.","- Use nested Stacks when you need explicit rows/sections.","- Show/hide sections: $editId != \"\" ? Card([editForm]) : null","- Modal: Modal(\"Title\", $showModal, [content]) — $showModal is boolean, X/Escape auto-closes. Put Form with its own buttons inside children.","- Use Tabs for alternative views (chart types, data sections) — no $variable needed","- Shared filter across Tabs: same $days binding in Query args works across all TabItems"]},{"name":"Content","components":["Card","CardHeader","TextContent","MarkDownRenderer","Callout","TextCallout","Image","ImageBlock","ImageGallery","CodeBlock","InlineHeader"],"notes":["- InlineHeader is a compact heading + description pair for labelling a block (lighter than CardHeader).","- Use Cards to group related KPIs or sections. Stack with direction \"row\" for side-by-side layouts.","- Success toast: Callout(\"success\", \"Saved\", \"Done.\", $showSuccess) — use @Set($showSuccess, true) in save action, auto-dismisses after 3s. For errors: result.status == \"error\" ? Callout(\"error\", \"Failed\", result.error) : null","- KPI card: Card([TextContent(\"Label\", \"small\"), TextContent(\"\" + @Count(@Filter(data.rows, \"field\", \"==\", \"value\")), \"large-heavy\")])"]},{"name":"Tables","components":["Table","Col","EditableTable"],"notes":["- EditableTable lets the user edit cells inline. Give it a unique name, columns of { type, key, header } with type one of text | number | date-single | select | url (select also needs options: [{ value, label }]).","- data is an array of { id, values } rows where values are ordered positionally to match columns. Edited data is submitted when the user clicks Save Changes.","- Table is COLUMN-oriented: Table([Col(\"Label\", dataArray), Col(\"Count\", countArray, \"number\")]). Use array pluck for data: data.rows.fieldName","- Col data can be component arrays for styled cells: Col(\"Status\", @Each(data.rows, \"item\", Tag(item.status, null, \"sm\", item.status == \"open\" ? \"success\" : \"danger\")))","- Row actions: Col(\"Actions\", @Each(data.rows, \"t\", Button(\"Edit\", Action([@Set($showEdit, true), @Set($editId, t.id)]))))","- Sortable: sorted = @Sort(data.rows, $sortField, \"desc\"). Bind $sortField to Select. Use sorted.fieldName for Col data","- Searchable: filtered = @Filter(data.rows, \"title\", \"contains\", $search). Bind $search to Input","- Chain sort + filter: filtered = @Filter(...) then sorted = @Sort(filtered, ...) — use sorted for both Table and Charts","- Empty state: @Count(data.rows) > 0 ? Table([...]) : TextContent(\"No data yet\")"]},{"name":"Charts (2D)","components":["BarChart","LineChart","AreaChart","RadarChart","HorizontalBarChart","Series"],"notes":["- Charts accept column arrays: LineChart(labels, [Series(\"Name\", values)]). Use array pluck: LineChart(data.rows.day, [Series(\"Views\", data.rows.views)])","- Use Cards to wrap charts with CardHeader for titled sections","- Chart + Table from same source: use @Sort or @Filter result for both LineChart and Table Col data","- Multiple chart views: use Tabs — Tabs([TabItem(\"line\", \"Line\", [LineChart(...)]), TabItem(\"bar\", \"Bar\", [BarChart(...)])])"]},{"name":"Charts (1D)","components":["PieChart","RadialChart","SingleStackedBarChart","Slice"],"notes":["- PieChart and BarChart need NUMBERS, not objects. For list data, use @Count(@Filter(...)) to aggregate:","- PieChart from list: `PieChart([\"Low\", \"Med\", \"High\"], [@Count(@Filter(data.rows, \"priority\", \"==\", \"low\")), @Count(@Filter(data.rows, \"priority\", \"==\", \"medium\")), @Count(@Filter(data.rows, \"priority\", \"==\", \"high\"))], \"donut\")`","- KPI from count: `TextContent(\"\" + @Count(@Filter(data.rows, \"status\", \"==\", \"open\")), \"large-heavy\")`"]},{"name":"Charts (Scatter)","components":["ScatterChart","ScatterSeries","Point"]},{"name":"Forms","components":["Form","FormControl","Label","Input","TextArea","Select","SelectItem","DatePicker","Slider","CheckBoxGroup","CheckBoxItem","RadioGroup","RadioItem","SwitchGroup","SwitchItem","Chips","ChipItem","OptionCards","OptionCard"],"notes":["- Chips: compact single/multiple selection pills. Use ChipItem references for each option.","- OptionCards: larger selectable cards with title, subtitle and an optional Icon or Image on top. Use OptionCard references for each option.","- For Form fields, define EACH FormControl as its own reference — do NOT inline all controls in one array. This allows progressive field-by-field streaming.","- NEVER nest Form inside Form — each Form should be a standalone container.","- Form requires explicit buttons. Always pass a Buttons(...) reference as the third Form argument.","- rules is an optional object: {required: true, email: true, minLength: 8, maxLength: 100}","- Available rules: required, email, min, max, minLength, maxLength, pattern, url, numeric","- The renderer shows error messages automatically — do NOT generate error text in the UI","- Conditional fields: $country == \"US\" ? stateField : $country == \"UK\" ? postcodeField : addressField","- Edit form in Modal: Modal(\"Edit\", $showEdit, [Form(\"edit\", Buttons([saveBtn, cancelBtn]), [fields...])]). Save button should include @Set($showEdit, false) to close modal."]},{"name":"Buttons","components":["Button","Buttons","IconButton"],"notes":["- Icon renders a lucide icon by kebab-case name; it is also used as the icon of IconButton, IconText and OptionCard.","- Toggle in @Each: @Each(rows, \"t\", Button(t.status == \"open\" ? \"Close\" : \"Reopen\", Action([...])))"]},{"name":"Data Display","components":["TagBlock","Tag","Icon","EntityList","ListBlock","ListItem"],"notes":["- Color-mapped Tag: Tag(value, null, \"sm\", value == \"high\" ? \"danger\" : value == \"medium\" ? \"warning\" : \"neutral\")","- EntityList is a compact two-column list of { left, right } rows (e.g. name / value). size='default' also supports a header and footer row; size='small' does not.","- ListBlock is a numbered or image list of ListItem references. An action on ListItem is optional."]},{"name":"Cards","components":["SnippetCardBlock","SnippetCardItem","OverviewCardBlock","OverviewCardItem","ContextCardBlock","ContextCardItem","CompositeCardBlock","CompositeCardItem","VisualCardBlock","VisualCardItem","Text","BoldText","IconText","ImageText","ImageTextLarge","MetricIndicatorInline","MetricIndicatorWithStrikethrough"],"notes":["- Card blocks lay out 2+ items in a responsive grid (or carousel where supported). Every item in a block must have the same structure.","- SnippetCardItem: small card with lhs (IconText | ImageText) and optional rhs (Text | BoldText) — good for key/value facts.","- OverviewCardItem: small card with top (IconText | ImageText | Text) and optional bottom MetricIndicatorInline — good for KPIs.","- ContextCardItem: medium card with a title (string or Tag), body text and optional background image — good for summaries.","- CompositeCardItem: rich card with header, body array (Text, BoldText, MetricIndicatorInline, IconText, Image, charts, ListBlock, TagBlock, EntityList) and footer (price + Button) — good for products/offers.","- VisualCardItem: image-first card with a BoldText body and optional Tag.","- Text / BoldText / IconText / ImageText / ImageTextLarge / MetricIndicator* are the inline building blocks used INSIDE card items; do not place them directly in the root Card."]}],"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"Card":{"$ref":"#/$defs/Card"},"CardHeader":{"$ref":"#/$defs/CardHeader"},"TextContent":{"$ref":"#/$defs/TextContent"},"MarkDownRenderer":{"$ref":"#/$defs/MarkDownRenderer"},"Callout":{"$ref":"#/$defs/Callout"},"TextCallout":{"$ref":"#/$defs/TextCallout"},"Image":{"$ref":"#/$defs/Image"},"ImageBlock":{"$ref":"#/$defs/ImageBlock"},"ImageGallery":{"$ref":"#/$defs/ImageGallery"},"CodeBlock":{"$ref":"#/$defs/CodeBlock"},"InlineHeader":{"$ref":"#/$defs/InlineHeader"},"Table":{"$ref":"#/$defs/Table"},"Col":{"$ref":"#/$defs/Col"},"EditableTable":{"$ref":"#/$defs/EditableTable"},"BarChart":{"$ref":"#/$defs/BarChart"},"LineChart":{"$ref":"#/$defs/LineChart"},"AreaChart":{"$ref":"#/$defs/AreaChart"},"RadarChart":{"$ref":"#/$defs/RadarChart"},"HorizontalBarChart":{"$ref":"#/$defs/HorizontalBarChart"},"Series":{"$ref":"#/$defs/Series"},"PieChart":{"$ref":"#/$defs/PieChart"},"RadialChart":{"$ref":"#/$defs/RadialChart"},"SingleStackedBarChart":{"$ref":"#/$defs/SingleStackedBarChart"},"Slice":{"$ref":"#/$defs/Slice"},"ScatterChart":{"$ref":"#/$defs/ScatterChart"},"ScatterSeries":{"$ref":"#/$defs/ScatterSeries"},"Point":{"$ref":"#/$defs/Point"},"Form":{"$ref":"#/$defs/Form"},"FormControl":{"$ref":"#/$defs/FormControl"},"Label":{"$ref":"#/$defs/Label"},"Input":{"$ref":"#/$defs/Input"},"TextArea":{"$ref":"#/$defs/TextArea"},"Select":{"$ref":"#/$defs/Select"},"SelectItem":{"$ref":"#/$defs/SelectItem"},"DatePicker":{"$ref":"#/$defs/DatePicker"},"Slider":{"$ref":"#/$defs/Slider"},"CheckBoxGroup":{"$ref":"#/$defs/CheckBoxGroup"},"CheckBoxItem":{"$ref":"#/$defs/CheckBoxItem"},"RadioGroup":{"$ref":"#/$defs/RadioGroup"},"RadioItem":{"$ref":"#/$defs/RadioItem"},"SwitchGroup":{"$ref":"#/$defs/SwitchGroup"},"SwitchItem":{"$ref":"#/$defs/SwitchItem"},"ChipItem":{"$ref":"#/$defs/ChipItem"},"Chips":{"$ref":"#/$defs/Chips"},"OptionCard":{"$ref":"#/$defs/OptionCard"},"OptionCards":{"$ref":"#/$defs/OptionCards"},"Button":{"$ref":"#/$defs/Button"},"Buttons":{"$ref":"#/$defs/Buttons"},"IconButton":{"$ref":"#/$defs/IconButton"},"Stack":{"$ref":"#/$defs/Stack"},"Tabs":{"$ref":"#/$defs/Tabs"},"TabItem":{"$ref":"#/$defs/TabItem"},"Accordion":{"$ref":"#/$defs/Accordion"},"AccordionItem":{"$ref":"#/$defs/AccordionItem"},"Steps":{"$ref":"#/$defs/Steps"},"StepsItem":{"$ref":"#/$defs/StepsItem"},"Carousel":{"$ref":"#/$defs/Carousel"},"Separator":{"$ref":"#/$defs/Separator"},"TagBlock":{"$ref":"#/$defs/TagBlock"},"Tag":{"$ref":"#/$defs/Tag"},"Icon":{"$ref":"#/$defs/Icon"},"EntityList":{"$ref":"#/$defs/EntityList"},"ListBlock":{"$ref":"#/$defs/ListBlock"},"ListItem":{"$ref":"#/$defs/ListItem"},"Text":{"$ref":"#/$defs/Text"},"BoldText":{"$ref":"#/$defs/BoldText"},"IconText":{"$ref":"#/$defs/IconText"},"ImageText":{"$ref":"#/$defs/ImageText"},"ImageTextLarge":{"$ref":"#/$defs/ImageTextLarge"},"MetricIndicatorInline":{"$ref":"#/$defs/MetricIndicatorInline"},"MetricIndicatorWithStrikethrough":{"$ref":"#/$defs/MetricIndicatorWithStrikethrough"},"SnippetCardItem":{"$ref":"#/$defs/SnippetCardItem"},"SnippetCardBlock":{"$ref":"#/$defs/SnippetCardBlock"},"OverviewCardItem":{"$ref":"#/$defs/OverviewCardItem"},"OverviewCardBlock":{"$ref":"#/$defs/OverviewCardBlock"},"ContextCardItem":{"$ref":"#/$defs/ContextCardItem"},"ContextCardBlock":{"$ref":"#/$defs/ContextCardBlock"},"CompositeCardItem":{"$ref":"#/$defs/CompositeCardItem"},"CompositeCardBlock":{"$ref":"#/$defs/CompositeCardBlock"},"VisualCardItem":{"$ref":"#/$defs/VisualCardItem"},"VisualCardBlock":{"$ref":"#/$defs/VisualCardBlock"},"Modal":{"$ref":"#/$defs/Modal"}},"required":["Card","CardHeader","TextContent","MarkDownRenderer","Callout","TextCallout","Image","ImageBlock","ImageGallery","CodeBlock","InlineHeader","Table","Col","EditableTable","BarChart","LineChart","AreaChart","RadarChart","HorizontalBarChart","Series","PieChart","RadialChart","SingleStackedBarChart","Slice","ScatterChart","ScatterSeries","Point","Form","FormControl","Label","Input","TextArea","Select","SelectItem","DatePicker","Slider","CheckBoxGroup","CheckBoxItem","RadioGroup","RadioItem","SwitchGroup","SwitchItem","ChipItem","Chips","OptionCard","OptionCards","Button","Buttons","IconButton","Stack","Tabs","TabItem","Accordion","AccordionItem","Steps","StepsItem","Carousel","Separator","TagBlock","Tag","Icon","EntityList","ListBlock","ListItem","Text","BoldText","IconText","ImageText","ImageTextLarge","MetricIndicatorInline","MetricIndicatorWithStrikethrough","SnippetCardItem","SnippetCardBlock","OverviewCardItem","OverviewCardBlock","ContextCardItem","ContextCardBlock","CompositeCardItem","CompositeCardBlock","VisualCardItem","VisualCardBlock","Modal"],"additionalProperties":false,"$defs":{"Card":{"type":"object","properties":{"children":{"type":"array","items":{"anyOf":[{"$ref":"#/$defs/TextContent"},{"$ref":"#/$defs/MarkDownRenderer"},{"$ref":"#/$defs/CardHeader"},{"$ref":"#/$defs/Callout"},{"$ref":"#/$defs/TextCallout"},{"$ref":"#/$defs/CodeBlock"},{"$ref":"#/$defs/Image"},{"$ref":"#/$defs/ImageBlock"},{"$ref":"#/$defs/ImageGallery"},{"$ref":"#/$defs/Separator"},{"$ref":"#/$defs/HorizontalBarChart"},{"$ref":"#/$defs/RadarChart"},{"$ref":"#/$defs/PieChart"},{"$ref":"#/$defs/RadialChart"},{"$ref":"#/$defs/SingleStackedBarChart"},{"$ref":"#/$defs/ScatterChart"},{"$ref":"#/$defs/AreaChart"},{"$ref":"#/$defs/BarChart"},{"$ref":"#/$defs/LineChart"},{"$ref":"#/$defs/Table"},{"$ref":"#/$defs/TagBlock"},{"$ref":"#/$defs/Form"},{"$ref":"#/$defs/Buttons"},{"$ref":"#/$defs/IconButton"},{"$ref":"#/$defs/Steps"},{"$ref":"#/$defs/InlineHeader"},{"$ref":"#/$defs/EntityList"},{"$ref":"#/$defs/EditableTable"},{"$ref":"#/$defs/SnippetCardBlock"},{"$ref":"#/$defs/OverviewCardBlock"},{"$ref":"#/$defs/ContextCardBlock"},{"$ref":"#/$defs/CompositeCardBlock"},{"$ref":"#/$defs/VisualCardBlock"},{"$ref":"#/$defs/Tabs"},{"$ref":"#/$defs/Carousel"},{"$ref":"#/$defs/Stack"}]}},"variant":{"type":"string","enum":["card","sunk","clear"]},"direction":{"type":"string","enum":["row","column"]},"gap":{"type":"string","enum":["none","xs","s","m","l","xl","2xl"]},"align":{"type":"string","enum":["start","center","end","stretch","baseline"]},"justify":{"type":"string","enum":["start","center","end","between","around","evenly"]},"wrap":{"type":"boolean"}},"required":["children"],"additionalProperties":false,"description":"Styled container. variant: \"card\" (default, elevated) | \"sunk\" (recessed) | \"clear\" (transparent). Always full width. Accepts all Stack flex params (default: direction \"column\"). Cards flex to share space in row/wrap layouts."},"TextContent":{"type":"object","properties":{"text":{"type":"string"},"size":{"type":"string","enum":["small","default","large","small-heavy","large-heavy"]}},"required":["text"],"additionalProperties":false,"description":"Text block. Supports markdown. Optional size: \"small\" | \"default\" | \"large\" | \"small-heavy\" | \"large-heavy\"."},"MarkDownRenderer":{"type":"object","properties":{"textMarkdown":{"type":"string"},"variant":{"type":"string","enum":["clear","card","sunk"]}},"required":["textMarkdown"],"additionalProperties":false,"description":"Renders markdown text with optional container variant"},"CardHeader":{"type":"object","properties":{"title":{"type":"string"},"subtitle":{"type":"string"}},"additionalProperties":false,"description":"Header with optional title and subtitle"},"Callout":{"type":"object","properties":{"variant":{"type":"string","enum":["info","warning","error","success","neutral"]},"title":{"type":"string"},"description":{"type":"string"},"visible":{"type":"boolean"}},"required":["variant","title","description"],"additionalProperties":false,"description":"Callout banner. Optional visible is a reactive $boolean — auto-dismisses after 3s by setting $visible to false."},"TextCallout":{"type":"object","properties":{"variant":{"type":"string","enum":["neutral","info","warning","success","danger"]},"title":{"type":"string"},"description":{"type":"string"}},"additionalProperties":false,"description":"Text callout with variant, title, and description"},"CodeBlock":{"type":"object","properties":{"language":{"type":"string"},"codeString":{"type":"string"}},"required":["language","codeString"],"additionalProperties":false,"description":"Syntax-highlighted code block"},"Image":{"type":"object","properties":{"alt":{"type":"string"},"src":{"type":"string"}},"required":["alt"],"additionalProperties":false,"description":"Image with alt text and optional URL"},"ImageBlock":{"type":"object","properties":{"src":{"type":"string"},"alt":{"type":"string"}},"required":["src"],"additionalProperties":false,"description":"Image block with loading state"},"ImageGallery":{"type":"object","properties":{"images":{"type":"array","items":{"type":"object","properties":{"src":{"type":"string"},"alt":{"type":"string"},"details":{"type":"string"}},"required":["src"],"additionalProperties":false}}},"required":["images"],"additionalProperties":false,"description":"Gallery grid of images with modal preview"},"Separator":{"type":"object","properties":{"orientation":{"type":"string","enum":["horizontal","vertical"]},"decorative":{"type":"boolean"}},"additionalProperties":false,"description":"Visual divider between content sections"},"HorizontalBarChart":{"type":"object","properties":{"labels":{"type":"array","items":{"type":"string"}},"series":{"type":"array","items":{"$ref":"#/$defs/Series"}},"variant":{"type":"string","enum":["grouped","stacked"]},"xLabel":{"type":"string"},"yLabel":{"type":"string"}},"required":["labels","series"],"additionalProperties":false,"description":"Horizontal bars; prefer when category labels are long or for ranked lists"},"Series":{"type":"object","properties":{"category":{"type":"string"},"values":{"type":"array","items":{"type":"number"}}},"required":["category","values"],"additionalProperties":false,"description":"One data series"},"RadarChart":{"type":"object","properties":{"labels":{"type":"array","items":{"type":"string"}},"series":{"type":"array","items":{"$ref":"#/$defs/Series"}}},"required":["labels","series"],"additionalProperties":false,"description":"Spider/web chart; use for comparing multiple variables across one or more entities"},"PieChart":{"type":"object","properties":{"labels":{"type":"array","items":{"type":"string"}},"values":{"type":"array","items":{"type":"number"}},"variant":{"type":"string","enum":["pie","donut"]},"appearance":{"type":"string","enum":["circular","semiCircular"]}},"required":["labels","values"],"additionalProperties":false,"description":"Circular slices; use plucked arrays: PieChart(data.categories, data.values)"},"RadialChart":{"type":"object","properties":{"labels":{"type":"array","items":{"type":"string"}},"values":{"type":"array","items":{"type":"number"}}},"required":["labels","values"],"additionalProperties":false,"description":"Radial bars; use plucked arrays: RadialChart(data.categories, data.values)"},"SingleStackedBarChart":{"type":"object","properties":{"labels":{"type":"array","items":{"type":"string"}},"values":{"type":"array","items":{"type":"number"}}},"required":["labels","values"],"additionalProperties":false,"description":"Single horizontal stacked bar; use plucked arrays: SingleStackedBarChart(data.categories, data.values)"},"ScatterChart":{"type":"object","properties":{"datasets":{"type":"array","items":{"$ref":"#/$defs/ScatterSeries"}},"xLabel":{"type":"string"},"yLabel":{"type":"string"}},"required":["datasets"],"additionalProperties":false,"description":"X/Y scatter plot; use for correlations, distributions, and clustering"},"ScatterSeries":{"type":"object","properties":{"name":{"type":"string"},"points":{"type":"array","items":{"$ref":"#/$defs/Point"}}},"required":["name","points"],"additionalProperties":false,"description":"Named dataset"},"Point":{"type":"object","properties":{"x":{"type":"number"},"y":{"type":"number"},"z":{"type":"number"}},"required":["x","y"],"additionalProperties":false,"description":"Data point with numeric coordinates"},"AreaChart":{"type":"object","properties":{"labels":{"type":"array","items":{"type":"string"}},"series":{"type":"array","items":{"$ref":"#/$defs/Series"}},"variant":{"type":"string","enum":["linear","natural","step"]},"xLabel":{"type":"string"},"yLabel":{"type":"string"},"height":{"type":"number"}},"required":["labels","series"],"additionalProperties":false,"description":"Filled area under lines; use for cumulative totals or volume trends over time"},"BarChart":{"type":"object","properties":{"labels":{"type":"array","items":{"type":"string"}},"series":{"type":"array","items":{"$ref":"#/$defs/Series"}},"variant":{"type":"string","enum":["grouped","stacked"]},"xLabel":{"type":"string"},"yLabel":{"type":"string"},"height":{"type":"number"}},"required":["labels","series"],"additionalProperties":false,"description":"Vertical bars; use for comparing values across categories with one or more series"},"LineChart":{"type":"object","properties":{"labels":{"type":"array","items":{"type":"string"}},"series":{"type":"array","items":{"$ref":"#/$defs/Series"}},"variant":{"type":"string","enum":["linear","natural","step"]},"xLabel":{"type":"string"},"yLabel":{"type":"string"},"height":{"type":"number"}},"required":["labels","series"],"additionalProperties":false,"description":"Lines over categories; use for trends and continuous data over time"},"Table":{"type":"object","properties":{"columns":{"type":"array","items":{"$ref":"#/$defs/Col"}}},"required":["columns"],"additionalProperties":false,"description":"Data table — column-oriented. Each Col holds its own data array."},"Col":{"type":"object","properties":{"label":{"type":"string"},"data":{},"type":{"type":"string","enum":["string","number","action"]}},"required":["label","data"],"additionalProperties":false,"description":"Column definition — holds label + data array"},"TagBlock":{"type":"object","properties":{"tags":{"type":"array","items":{"type":"string"}},"size":{"type":"string","enum":["sm","md","lg"]}},"required":["tags"],"additionalProperties":false,"description":"tags is an array of strings; optional size sm | md | lg"},"Form":{"type":"object","properties":{"name":{"type":"string"},"buttons":{"$ref":"#/$defs/Buttons"},"fields":{"default":[],"type":"array","items":{"$ref":"#/$defs/FormControl"}}},"required":["name","buttons","fields"],"additionalProperties":false,"description":"Form container with fields and explicit action buttons"},"Buttons":{"type":"object","properties":{"buttons":{"type":"array","items":{"$ref":"#/$defs/Button"}},"direction":{"type":"string","enum":["row","column"]}},"required":["buttons"],"additionalProperties":false,"description":"Group of Button components. direction: \"row\" (default) | \"column\"."},"Button":{"type":"object","properties":{"label":{"type":"string"},"action":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","const":"open_url"},"url":{"type":"string"}},"required":["type","url"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string","const":"continue_conversation"},"context":{"type":"string"}},"required":["type"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string"},"params":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["type"],"additionalProperties":false}]},"variant":{"type":"string","enum":["primary","secondary","tertiary"]},"type":{"type":"string","enum":["normal","destructive"]},"size":{"type":"string","enum":["extra-small","small","medium","large"]}},"required":["label"],"additionalProperties":false,"description":"Clickable button"},"FormControl":{"type":"object","properties":{"label":{"type":"string"},"input":{"anyOf":[{"$ref":"#/$defs/Input"},{"$ref":"#/$defs/TextArea"},{"$ref":"#/$defs/Select"},{"$ref":"#/$defs/DatePicker"},{"$ref":"#/$defs/Slider"},{"$ref":"#/$defs/CheckBoxGroup"},{"$ref":"#/$defs/RadioGroup"},{"$ref":"#/$defs/Chips"},{"$ref":"#/$defs/OptionCards"}]},"hint":{"type":"string"}},"required":["label","input"],"additionalProperties":false,"description":"Field with label, input component, and optional hint text"},"Input":{"type":"object","properties":{"name":{"type":"string"},"placeholder":{"type":"string"},"type":{"type":"string","enum":["text","email","password","number","url"]},"rules":{"type":"object","properties":{"required":{"type":"boolean"},"email":{"type":"boolean"},"url":{"type":"boolean"},"numeric":{"type":"boolean"},"min":{"type":"number"},"max":{"type":"number"},"minLength":{"type":"number"},"maxLength":{"type":"number"},"pattern":{"type":"string"}},"additionalProperties":false},"value":{"type":"string"}},"required":["name"],"additionalProperties":false},"TextArea":{"type":"object","properties":{"name":{"type":"string"},"placeholder":{"type":"string"},"rows":{"type":"number"},"rules":{"type":"object","properties":{"required":{"type":"boolean"},"email":{"type":"boolean"},"url":{"type":"boolean"},"numeric":{"type":"boolean"},"min":{"type":"number"},"max":{"type":"number"},"minLength":{"type":"number"},"maxLength":{"type":"number"},"pattern":{"type":"string"}},"additionalProperties":false},"value":{"type":"string"}},"required":["name"],"additionalProperties":false},"Select":{"type":"object","properties":{"name":{"type":"string"},"items":{"type":"array","items":{"$ref":"#/$defs/SelectItem"}},"placeholder":{"type":"string"},"rules":{"type":"object","properties":{"required":{"type":"boolean"},"email":{"type":"boolean"},"url":{"type":"boolean"},"numeric":{"type":"boolean"},"min":{"type":"number"},"max":{"type":"number"},"minLength":{"type":"number"},"maxLength":{"type":"number"},"pattern":{"type":"string"}},"additionalProperties":false},"value":{"type":"string"},"size":{"type":"string","enum":["small","medium","large"]}},"required":["name","items"],"additionalProperties":false},"SelectItem":{"type":"object","properties":{"value":{"type":"string"},"label":{"type":"string"}},"required":["value","label"],"additionalProperties":false,"description":"Option for Select"},"DatePicker":{"type":"object","properties":{"name":{"type":"string"},"mode":{"type":"string","enum":["single","range"]},"rules":{"type":"object","properties":{"required":{"type":"boolean"},"email":{"type":"boolean"},"url":{"type":"boolean"},"numeric":{"type":"boolean"},"min":{"type":"number"},"max":{"type":"number"},"minLength":{"type":"number"},"maxLength":{"type":"number"},"pattern":{"type":"string"}},"additionalProperties":false},"value":{}},"required":["name"],"additionalProperties":false},"Slider":{"type":"object","properties":{"name":{"type":"string"},"variant":{"type":"string","enum":["continuous","discrete"]},"min":{"type":"number"},"max":{"type":"number"},"step":{"type":"number"},"defaultValue":{"type":"array","items":{"type":"number"}},"label":{"type":"string"},"rules":{"type":"object","properties":{"required":{"type":"boolean"},"email":{"type":"boolean"},"url":{"type":"boolean"},"numeric":{"type":"boolean"},"min":{"type":"number"},"max":{"type":"number"},"minLength":{"type":"number"},"maxLength":{"type":"number"},"pattern":{"type":"string"}},"additionalProperties":false},"value":{"type":"array","items":{"type":"number"}}},"required":["name","variant","min","max"],"additionalProperties":false,"description":"Numeric slider input; supports continuous and discrete (stepped) variants"},"CheckBoxGroup":{"type":"object","properties":{"name":{"type":"string"},"items":{"type":"array","items":{"$ref":"#/$defs/CheckBoxItem"}},"rules":{"type":"object","properties":{"required":{"type":"boolean"},"email":{"type":"boolean"},"url":{"type":"boolean"},"numeric":{"type":"boolean"},"min":{"type":"number"},"max":{"type":"number"},"minLength":{"type":"number"},"maxLength":{"type":"number"},"pattern":{"type":"string"}},"additionalProperties":false},"value":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"boolean"}}},"required":["name","items"],"additionalProperties":false},"CheckBoxItem":{"type":"object","properties":{"label":{"type":"string"},"description":{"type":"string"},"name":{"type":"string"},"defaultChecked":{"type":"boolean"}},"required":["label","description","name"],"additionalProperties":false},"RadioGroup":{"type":"object","properties":{"name":{"type":"string"},"items":{"type":"array","items":{"$ref":"#/$defs/RadioItem"}},"defaultValue":{"type":"string"},"rules":{"type":"object","properties":{"required":{"type":"boolean"},"email":{"type":"boolean"},"url":{"type":"boolean"},"numeric":{"type":"boolean"},"min":{"type":"number"},"max":{"type":"number"},"minLength":{"type":"number"},"maxLength":{"type":"number"},"pattern":{"type":"string"}},"additionalProperties":false},"value":{"type":"string"}},"required":["name","items"],"additionalProperties":false},"RadioItem":{"type":"object","properties":{"label":{"type":"string"},"description":{"type":"string"},"value":{"type":"string"}},"required":["label","description","value"],"additionalProperties":false},"Chips":{"type":"object","properties":{"name":{"type":"string"},"type":{"default":"multiple","type":"string","enum":["single","multiple"]},"items":{"default":[],"type":"array","items":{"$ref":"#/$defs/ChipItem"}},"rules":{"type":"object","properties":{"required":{"type":"boolean"},"email":{"type":"boolean"},"url":{"type":"boolean"},"numeric":{"type":"boolean"},"min":{"type":"number"},"max":{"type":"number"},"minLength":{"type":"number"},"maxLength":{"type":"number"},"pattern":{"type":"string"}},"additionalProperties":false},"defaultValue":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]}},"required":["name","type","items"],"additionalProperties":false,"description":"A form field of compact selectable chips for choosing one or many short options; the selection is stored under `name`."},"ChipItem":{"type":"object","properties":{"value":{"type":"string"},"label":{"type":"string"},"icon":{"$ref":"#/$defs/Icon"},"disabled":{"type":"boolean"}},"required":["value","label"],"additionalProperties":false,"description":"A single selectable chip inside a Chips group, with a value, label and optional icon."},"Icon":{"type":"object","properties":{"name":{"type":"string"},"category":{"type":"string"}},"required":["name"],"additionalProperties":false,"description":"A lucide icon by kebab-case name (e.g. 'circle-check'). Optional category picks a topical fallback when the name doesn't resolve."},"OptionCards":{"type":"object","properties":{"name":{"type":"string"},"type":{"default":"single","type":"string","enum":["single","multiple"]},"items":{"default":[],"type":"array","items":{"$ref":"#/$defs/OptionCard"}},"rules":{"type":"object","properties":{"required":{"type":"boolean"},"email":{"type":"boolean"},"url":{"type":"boolean"},"numeric":{"type":"boolean"},"min":{"type":"number"},"max":{"type":"number"},"minLength":{"type":"number"},"maxLength":{"type":"number"},"pattern":{"type":"string"}},"additionalProperties":false},"defaultValue":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]}},"required":["name","type","items"],"additionalProperties":false,"description":"A form field of selectable cards (title, optional subtitle, optional icon or image) laid out in a responsive grid for choosing one or many options; the selection is stored under `name`."},"OptionCard":{"type":"object","properties":{"value":{"type":"string"},"title":{"type":"string"},"subtitle":{"type":"string"},"topContent":{"anyOf":[{"$ref":"#/$defs/Icon"},{"$ref":"#/$defs/Image"}]},"disabled":{"type":"boolean"}},"required":["value","title"],"additionalProperties":false,"description":"A single selectable card inside an OptionCards group, with a value, title, optional subtitle and an optional Icon or Image on top."},"IconButton":{"type":"object","properties":{"name":{"type":"string"},"icon":{"$ref":"#/$defs/Icon"},"action":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","const":"open_url"},"url":{"type":"string"}},"required":["type","url"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string","const":"continue_conversation"},"context":{"type":"string"}},"required":["type"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string"},"params":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["type"],"additionalProperties":false}]},"variant":{"type":"string","enum":["primary","secondary","tertiary"]},"size":{"type":"string","enum":["extra-small","small","medium","large"]},"shape":{"type":"string","enum":["square","circle"]}},"required":["name","icon"],"additionalProperties":false,"description":"Icon-only button. name is the accessible label and the action label; icon is an Icon; action fires on click."},"Steps":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/$defs/StepsItem"}}},"required":["items"],"additionalProperties":false,"description":"Step-by-step guide"},"StepsItem":{"type":"object","properties":{"title":{"type":"string"},"details":{"type":"string"}},"required":["title","details"],"additionalProperties":false,"description":"title and details text for one step"},"InlineHeader":{"type":"object","properties":{"heading":{"type":"string"},"description":{"type":"string"}},"required":["heading"],"additionalProperties":false,"description":"Compact section heading with an optional one-line description, for use inside cards."},"EntityList":{"type":"object","properties":{"rows":{"default":[],"type":"array","items":{"type":"object","properties":{"left":{"type":"string"},"right":{"type":"string"},"rightVariant":{"default":"text","type":"string","enum":["text","number"]}},"required":["left","right","rightVariant"],"additionalProperties":false}},"size":{"default":"default","type":"string","enum":["small","default"]},"header":{"type":"object","properties":{"left":{"type":"string"},"right":{"type":"string"},"rightVariant":{"default":"text","type":"string","enum":["text","number"]}},"required":["left","right","rightVariant"],"additionalProperties":false},"footer":{"type":"object","properties":{"left":{"type":"string"},"right":{"type":"string"},"rightVariant":{"default":"text","type":"string","enum":["text","number"]}},"required":["left","right","rightVariant"],"additionalProperties":false}},"required":["rows","size"],"additionalProperties":false,"description":"Two-column key/value rows (left label, right value). size 'default' supports optional header and footer rows; rightVariant 'number' uses tabular numbers."},"EditableTable":{"type":"object","properties":{"name":{"default":"","type":"string"},"columns":{"default":[],"type":"array","items":{"type":"object","properties":{"type":{"type":"string","enum":["text","number","date-single","select","url"]},"key":{"default":"default","type":"string"},"header":{"default":"","type":"string"},"width":{"type":"number"},"options":{"type":"array","items":{"type":"object","properties":{"value":{"type":"string"},"label":{"type":"string"}},"required":["value","label"],"additionalProperties":false}}},"required":["type","key","header"],"additionalProperties":false}},"data":{"default":[],"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"values":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}}},"required":["id","values"],"additionalProperties":false}}},"required":["name","columns","data"],"additionalProperties":false,"description":"Spreadsheet-like table whose cells the user can edit inline (text, number, url, date, select columns); edits are saved back as a form field"},"SnippetCardBlock":{"type":"object","properties":{"items":{"minItems":2,"type":"array","items":{"$ref":"#/$defs/SnippetCardItem"}},"layout":{"default":"grid","type":"string","enum":["grid"]},"responsive":{"default":true,"type":"boolean"},"action":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","const":"open_url"},"url":{"type":"string"}},"required":["type","url"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string","const":"continue_conversation"},"context":{"type":"string"}},"required":["type"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string"},"params":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["type"],"additionalProperties":false}]},"gap":{"anyOf":[{"type":"number"},{"type":"string"}]}},"required":["items","layout","responsive"],"additionalProperties":false,"description":"A responsive grid of compact label/value cards (2 per row) for showing several short facts side by side; optionally clickable with a shared action."},"SnippetCardItem":{"type":"object","properties":{"id":{"type":"string"},"lhs":{"anyOf":[{"$ref":"#/$defs/IconText"},{"$ref":"#/$defs/ImageText"}]},"rhs":{"anyOf":[{"$ref":"#/$defs/Text"},{"$ref":"#/$defs/BoldText"}]}},"required":["lhs"],"additionalProperties":false,"description":"One row-style snippet card: a label on the left (IconText or ImageText) and an optional value on the right (Text or BoldText)."},"IconText":{"type":"object","properties":{"icon":{"$ref":"#/$defs/Icon"},"iconVariant":{"default":"neutral","type":"string","enum":["neutral","info","success","warning","danger","inverted","filled","soft"]},"iconSize":{"default":"m","type":"string","enum":["xs","s","m","l","xl","sm","md","lg"]},"title":{"type":"string"},"subtitle":{"type":"string"},"bold":{"default":false,"type":"boolean"},"layout":{"default":"horizontal","type":"string","enum":["horizontal","vertical"]}},"required":["icon","iconVariant","iconSize","title","bold","layout"],"additionalProperties":false,"description":"An icon badge with a title and optional subtitle, laid out horizontally or vertically. iconVariant sets the badge color."},"ImageText":{"type":"object","properties":{"src":{"type":"string"},"alt":{"type":"string"},"title":{"type":"string"},"subtitle":{"type":"string"},"bold":{"default":false,"type":"boolean"},"layout":{"default":"horizontal","type":"string","enum":["horizontal","vertical"]},"imageSize":{"type":"number"}},"required":["src","title","bold","layout"],"additionalProperties":false,"description":"A small square image (thumbnail/avatar) with a title and optional subtitle. src must be a real image URL."},"Text":{"type":"object","properties":{"variant":{"default":"text","type":"string","enum":["text","number"]},"value":{"type":"string"},"subtext":{"type":"string"},"subtextVariant":{"default":"text","type":"string","enum":["text","number","metric"]},"size":{"default":"sm","type":"string","enum":["xs","sm","md","lg"]}},"required":["variant","value","subtextVariant","size"],"additionalProperties":false,"description":"Plain text line with optional subtext. variant 'number' uses tabular number styling; subtextVariant 'metric' colors a leading +/- subtext green/red."},"BoldText":{"type":"object","properties":{"variant":{"default":"text","type":"string","enum":["text","number"]},"value":{"type":"string"},"subtext":{"type":"string"},"subtextVariant":{"default":"text","type":"string","enum":["text","number","metric"]},"size":{"default":"sm","type":"string","enum":["xs","sm","md","lg"]}},"required":["variant","value","subtextVariant","size"],"additionalProperties":false,"description":"Emphasized (bold) text line with optional subtext. variant 'number' uses tabular number styling; subtextVariant 'metric' colors a leading +/- subtext green/red."},"OverviewCardBlock":{"type":"object","properties":{"items":{"minItems":2,"type":"array","items":{"$ref":"#/$defs/OverviewCardItem"}},"layout":{"default":"grid","type":"string","enum":["grid","carousel"]},"responsive":{"default":true,"type":"boolean"},"action":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","const":"open_url"},"url":{"type":"string"}},"required":["type","url"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string","const":"continue_conversation"},"context":{"type":"string"}},"required":["type"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string"},"params":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["type"],"additionalProperties":false}]},"gap":{"anyOf":[{"type":"number"},{"type":"string"}]}},"required":["items","layout","responsive"],"additionalProperties":false,"description":"A grid or horizontal carousel of compact overview cards, each with a heading (icon/image/text) on top and an inline metric below; optionally clickable with a shared action."},"OverviewCardItem":{"type":"object","properties":{"id":{"type":"string"},"top":{"anyOf":[{"$ref":"#/$defs/IconText"},{"$ref":"#/$defs/ImageText"},{"$ref":"#/$defs/Text"}]},"bottom":{"$ref":"#/$defs/MetricIndicatorInline"}},"required":["top"],"additionalProperties":false,"description":"One overview card: a heading slot at the top (IconText, ImageText or Text) and an optional MetricIndicatorInline at the bottom."},"MetricIndicatorInline":{"type":"object","properties":{"value":{"type":"string"},"subtext":{"type":"string"},"trend":{"type":"object","properties":{"direction":{"type":"string","enum":["up","down"]},"value":{"type":"number"}},"required":["direction","value"],"additionalProperties":false}},"required":["value"],"additionalProperties":false,"description":"Headline metric value with an optional +/- percentage trend and subtext, all on one line."},"ContextCardBlock":{"type":"object","properties":{"items":{"minItems":2,"type":"array","items":{"$ref":"#/$defs/ContextCardItem"}},"layout":{"default":"grid","type":"string","enum":["grid","carousel"]},"responsive":{"default":true,"type":"boolean"},"action":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","const":"open_url"},"url":{"type":"string"}},"required":["type","url"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string","const":"continue_conversation"},"context":{"type":"string"}},"required":["type"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string"},"params":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["type"],"additionalProperties":false}]},"gap":{"anyOf":[{"type":"number"},{"type":"string"}]}},"required":["items","layout","responsive"],"additionalProperties":false,"description":"A grid or carousel of compact tinted context cards (title or tag plus a short bold body); an optional action makes every card clickable."},"ContextCardItem":{"type":"object","properties":{"id":{"type":"string"},"title":{"anyOf":[{"type":"string"},{"$ref":"#/$defs/Tag"}]},"body":{"type":"string"},"bgColor":{"type":"string","enum":["gray"]},"bgImageSrc":{"type":"string"},"bgImageAlt":{"type":"string"}},"required":["title"],"additionalProperties":false,"description":"A single card inside a ContextCardBlock: a title (plain string or Tag), an optional markdown body, and an optional gray tint or background image."},"Tag":{"type":"object","properties":{"text":{"type":"string"},"icon":{"$ref":"#/$defs/Icon"},"size":{"type":"string","enum":["sm","md","lg"]},"variant":{"type":"string","enum":["neutral","info","success","warning","danger"]}},"required":["text"],"additionalProperties":false,"description":"Styled tag/badge with optional Icon and variant"},"CompositeCardBlock":{"type":"object","properties":{"items":{"minItems":2,"type":"array","items":{"$ref":"#/$defs/CompositeCardItem"}},"layout":{"default":"grid","type":"string","enum":["grid","carousel"]},"responsive":{"default":true,"type":"boolean"},"action":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","const":"open_url"},"url":{"type":"string"}},"required":["type","url"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string","const":"continue_conversation"},"context":{"type":"string"}},"required":["type"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string"},"params":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["type"],"additionalProperties":false}]},"gap":{"anyOf":[{"type":"number"},{"type":"string"}]}},"required":["items","layout","responsive"],"additionalProperties":false,"description":"A two-per-row grid or carousel of rich cards, each with an optional header, stacked body content (text, metrics, charts, lists, tags) and a price/button footer; an optional action makes every card clickable."},"CompositeCardItem":{"type":"object","properties":{"id":{"type":"string"},"header":{"anyOf":[{"$ref":"#/$defs/IconText"},{"$ref":"#/$defs/ImageText"},{"$ref":"#/$defs/ImageTextLarge"},{"$ref":"#/$defs/Text"},{"$ref":"#/$defs/Image"}]},"body":{"default":[],"type":"array","items":{"anyOf":[{"$ref":"#/$defs/Text"},{"$ref":"#/$defs/BoldText"},{"$ref":"#/$defs/MetricIndicatorInline"},{"$ref":"#/$defs/IconText"},{"$ref":"#/$defs/Image"},{"$ref":"#/$defs/AreaChart"},{"$ref":"#/$defs/BarChart"},{"$ref":"#/$defs/LineChart"},{"$ref":"#/$defs/ListBlock"},{"$ref":"#/$defs/TagBlock"},{"$ref":"#/$defs/EntityList"}]}},"footer":{"type":"object","properties":{"price":{"anyOf":[{"$ref":"#/$defs/BoldText"},{"$ref":"#/$defs/MetricIndicatorWithStrikethrough"}]},"button":{"$ref":"#/$defs/Button"}},"additionalProperties":false}},"required":["body"],"additionalProperties":false,"description":"A single card inside a CompositeCardBlock: an optional header (icon/image/text), a stack of body elements (text, metrics, charts, lists, tags), and an optional price/button footer."},"ImageTextLarge":{"type":"object","properties":{"src":{"type":"string"},"alt":{"type":"string"},"title":{"type":"string"},"subtitle":{"type":"string"},"bold":{"default":false,"type":"boolean"}},"required":["src","title","bold"],"additionalProperties":false,"description":"A full-width banner image above a bold title and optional subtitle. src must be a real image URL."},"ListBlock":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/$defs/ListItem"}},"variant":{"type":"string","enum":["number","image"]},"size":{"type":"string","enum":["default","small"]}},"required":["items"],"additionalProperties":false,"description":"A list of items with number or image indicators. Each item can optionally have an action. size small renders a compact list."},"ListItem":{"type":"object","properties":{"title":{"type":"string"},"subtitle":{"type":"string"},"image":{"type":"object","properties":{"src":{"type":"string"},"alt":{"type":"string"}},"required":["src","alt"],"additionalProperties":false},"actionLabel":{"type":"string"},"action":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","const":"open_url"},"url":{"type":"string"}},"required":["type","url"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string","const":"continue_conversation"},"context":{"type":"string"}},"required":["type"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string"},"params":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["type"],"additionalProperties":false}]}},"required":["title"],"additionalProperties":false,"description":"Item in a ListBlock — displays a title with an optional subtitle and image. When action is provided, the item becomes clickable."},"MetricIndicatorWithStrikethrough":{"type":"object","properties":{"value":{"type":"string"},"subtext":{"type":"string"},"previousValue":{"type":"string"},"trend":{"type":"object","properties":{"direction":{"type":"string","enum":["up","down"]},"value":{"type":"number"}},"required":["direction","value"],"additionalProperties":false}},"required":["value"],"additionalProperties":false,"description":"Headline metric value with an optional struck-through previousValue, a +/- percentage trend, and subtext below."},"VisualCardBlock":{"type":"object","properties":{"items":{"minItems":2,"type":"array","items":{"$ref":"#/$defs/VisualCardItem"}},"layout":{"default":"grid","type":"string","enum":["grid","carousel"]},"responsive":{"default":true,"type":"boolean"},"action":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","const":"open_url"},"url":{"type":"string"}},"required":["type","url"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string","const":"continue_conversation"},"context":{"type":"string"}},"required":["type"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string"},"params":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["type"],"additionalProperties":false}]},"gap":{"anyOf":[{"type":"number"},{"type":"string"}]}},"required":["items","layout","responsive"],"additionalProperties":false,"description":"A grid or carousel of photo-first cards: a full-bleed background image with a tag on top and a bold text panel at the bottom; an optional action makes every card clickable."},"VisualCardItem":{"type":"object","properties":{"body":{"$ref":"#/$defs/BoldText"},"id":{"type":"string"},"bgImageSrc":{"type":"string"},"tag":{"$ref":"#/$defs/Tag"},"bgImageAlt":{"type":"string"}},"required":["body"],"additionalProperties":false,"description":"A single photo-first card inside a VisualCardBlock: a BoldText body panel, an optional Tag, and a background image (bgImageSrc must be a real URL; bgImageAlt is its alt text)."},"Tabs":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/$defs/TabItem"}}},"required":["items"],"additionalProperties":false,"description":"Tabbed container"},"TabItem":{"type":"object","properties":{"value":{"type":"string"},"trigger":{"type":"string"},"content":{"type":"array","items":{"anyOf":[{"$ref":"#/$defs/TextContent"},{"$ref":"#/$defs/MarkDownRenderer"},{"$ref":"#/$defs/CardHeader"},{"$ref":"#/$defs/Callout"},{"$ref":"#/$defs/TextCallout"},{"$ref":"#/$defs/CodeBlock"},{"$ref":"#/$defs/Image"},{"$ref":"#/$defs/ImageBlock"},{"$ref":"#/$defs/ImageGallery"},{"$ref":"#/$defs/Separator"},{"$ref":"#/$defs/HorizontalBarChart"},{"$ref":"#/$defs/RadarChart"},{"$ref":"#/$defs/PieChart"},{"$ref":"#/$defs/RadialChart"},{"$ref":"#/$defs/SingleStackedBarChart"},{"$ref":"#/$defs/ScatterChart"},{"$ref":"#/$defs/AreaChart"},{"$ref":"#/$defs/BarChart"},{"$ref":"#/$defs/LineChart"},{"$ref":"#/$defs/Table"},{"$ref":"#/$defs/TagBlock"},{"$ref":"#/$defs/Form"},{"$ref":"#/$defs/Buttons"},{"$ref":"#/$defs/IconButton"},{"$ref":"#/$defs/Steps"},{"$ref":"#/$defs/InlineHeader"},{"$ref":"#/$defs/EntityList"},{"$ref":"#/$defs/EditableTable"},{"$ref":"#/$defs/SnippetCardBlock"},{"$ref":"#/$defs/OverviewCardBlock"},{"$ref":"#/$defs/ContextCardBlock"},{"$ref":"#/$defs/CompositeCardBlock"},{"$ref":"#/$defs/VisualCardBlock"}]}}},"required":["value","trigger","content"],"additionalProperties":false,"description":"value is unique id, trigger is tab label, content is array of components"},"Carousel":{"type":"object","properties":{"children":{"type":"array","items":{"type":"array","items":{"anyOf":[{"$ref":"#/$defs/TextContent"},{"$ref":"#/$defs/MarkDownRenderer"},{"$ref":"#/$defs/CardHeader"},{"$ref":"#/$defs/Callout"},{"$ref":"#/$defs/TextCallout"},{"$ref":"#/$defs/CodeBlock"},{"$ref":"#/$defs/Image"},{"$ref":"#/$defs/ImageBlock"},{"$ref":"#/$defs/ImageGallery"},{"$ref":"#/$defs/Separator"},{"$ref":"#/$defs/HorizontalBarChart"},{"$ref":"#/$defs/RadarChart"},{"$ref":"#/$defs/PieChart"},{"$ref":"#/$defs/RadialChart"},{"$ref":"#/$defs/SingleStackedBarChart"},{"$ref":"#/$defs/ScatterChart"},{"$ref":"#/$defs/AreaChart"},{"$ref":"#/$defs/BarChart"},{"$ref":"#/$defs/LineChart"},{"$ref":"#/$defs/Table"},{"$ref":"#/$defs/TagBlock"},{"$ref":"#/$defs/Form"},{"$ref":"#/$defs/Buttons"},{"$ref":"#/$defs/IconButton"},{"$ref":"#/$defs/Steps"},{"$ref":"#/$defs/InlineHeader"},{"$ref":"#/$defs/EntityList"},{"$ref":"#/$defs/EditableTable"},{"$ref":"#/$defs/SnippetCardBlock"},{"$ref":"#/$defs/OverviewCardBlock"},{"$ref":"#/$defs/ContextCardBlock"},{"$ref":"#/$defs/CompositeCardBlock"},{"$ref":"#/$defs/VisualCardBlock"}]}}},"variant":{"type":"string","enum":["card","sunk"]}},"required":["children"],"additionalProperties":false,"description":"Horizontal scrollable carousel"},"Stack":{"type":"object","properties":{"children":{"type":"array","items":{}},"direction":{"type":"string","enum":["row","column"]},"gap":{"type":"string","enum":["none","xs","s","m","l","xl","2xl"]},"align":{"type":"string","enum":["start","center","end","stretch","baseline"]},"justify":{"type":"string","enum":["start","center","end","between","around","evenly"]},"wrap":{"type":"boolean"}},"required":["children"],"additionalProperties":false,"description":"Flex container. direction: \"row\"|\"column\" (default \"column\"). gap: \"none\"|\"xs\"|\"s\"|\"m\"|\"l\"|\"xl\"|\"2xl\" (default \"m\"). align: \"start\"|\"center\"|\"end\"|\"stretch\"|\"baseline\". justify: \"start\"|\"center\"|\"end\"|\"between\"|\"around\"|\"evenly\"."},"Slice":{"type":"object","properties":{"category":{"type":"string"},"value":{"type":"number"}},"required":["category","value"],"additionalProperties":false,"description":"One slice with label and numeric value"},"Label":{"type":"object","properties":{"text":{"type":"string"}},"required":["text"],"additionalProperties":false,"description":"Text label"},"SwitchGroup":{"type":"object","properties":{"name":{"type":"string"},"items":{"type":"array","items":{"$ref":"#/$defs/SwitchItem"}},"variant":{"type":"string","enum":["clear","card","sunk"]},"value":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"boolean"}}},"required":["name","items"],"additionalProperties":false,"description":"Group of switch toggles"},"SwitchItem":{"type":"object","properties":{"label":{"type":"string"},"description":{"type":"string"},"name":{"type":"string"},"defaultChecked":{"type":"boolean"}},"required":["name"],"additionalProperties":false,"description":"Individual switch toggle"},"Accordion":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/$defs/AccordionItem"}}},"required":["items"],"additionalProperties":false,"description":"Collapsible sections"},"AccordionItem":{"type":"object","properties":{"value":{"type":"string"},"trigger":{"type":"string"},"content":{"type":"array","items":{"anyOf":[{"$ref":"#/$defs/TextContent"},{"$ref":"#/$defs/MarkDownRenderer"},{"$ref":"#/$defs/CardHeader"},{"$ref":"#/$defs/Callout"},{"$ref":"#/$defs/TextCallout"},{"$ref":"#/$defs/CodeBlock"},{"$ref":"#/$defs/Image"},{"$ref":"#/$defs/ImageBlock"},{"$ref":"#/$defs/ImageGallery"},{"$ref":"#/$defs/Separator"},{"$ref":"#/$defs/HorizontalBarChart"},{"$ref":"#/$defs/RadarChart"},{"$ref":"#/$defs/PieChart"},{"$ref":"#/$defs/RadialChart"},{"$ref":"#/$defs/SingleStackedBarChart"},{"$ref":"#/$defs/ScatterChart"},{"$ref":"#/$defs/AreaChart"},{"$ref":"#/$defs/BarChart"},{"$ref":"#/$defs/LineChart"},{"$ref":"#/$defs/Table"},{"$ref":"#/$defs/TagBlock"},{"$ref":"#/$defs/Form"},{"$ref":"#/$defs/Buttons"},{"$ref":"#/$defs/IconButton"},{"$ref":"#/$defs/Steps"},{"$ref":"#/$defs/InlineHeader"},{"$ref":"#/$defs/EntityList"},{"$ref":"#/$defs/EditableTable"},{"$ref":"#/$defs/SnippetCardBlock"},{"$ref":"#/$defs/OverviewCardBlock"},{"$ref":"#/$defs/ContextCardBlock"},{"$ref":"#/$defs/CompositeCardBlock"},{"$ref":"#/$defs/VisualCardBlock"}]}}},"required":["value","trigger","content"],"additionalProperties":false,"description":"value is unique id, trigger is section title"},"Modal":{"type":"object","properties":{"title":{"type":"string"},"open":{"type":"boolean"},"children":{"type":"array","items":{"anyOf":[{"$ref":"#/$defs/TextContent"},{"$ref":"#/$defs/MarkDownRenderer"},{"$ref":"#/$defs/CardHeader"},{"$ref":"#/$defs/Callout"},{"$ref":"#/$defs/TextCallout"},{"$ref":"#/$defs/CodeBlock"},{"$ref":"#/$defs/Image"},{"$ref":"#/$defs/ImageBlock"},{"$ref":"#/$defs/ImageGallery"},{"$ref":"#/$defs/Separator"},{"$ref":"#/$defs/HorizontalBarChart"},{"$ref":"#/$defs/RadarChart"},{"$ref":"#/$defs/PieChart"},{"$ref":"#/$defs/RadialChart"},{"$ref":"#/$defs/SingleStackedBarChart"},{"$ref":"#/$defs/ScatterChart"},{"$ref":"#/$defs/AreaChart"},{"$ref":"#/$defs/BarChart"},{"$ref":"#/$defs/LineChart"},{"$ref":"#/$defs/Table"},{"$ref":"#/$defs/TagBlock"},{"$ref":"#/$defs/Form"},{"$ref":"#/$defs/Buttons"},{"$ref":"#/$defs/IconButton"},{"$ref":"#/$defs/Steps"},{"$ref":"#/$defs/InlineHeader"},{"$ref":"#/$defs/EntityList"},{"$ref":"#/$defs/EditableTable"},{"$ref":"#/$defs/SnippetCardBlock"},{"$ref":"#/$defs/OverviewCardBlock"},{"$ref":"#/$defs/ContextCardBlock"},{"$ref":"#/$defs/CompositeCardBlock"},{"$ref":"#/$defs/VisualCardBlock"}]}},"size":{"type":"string","enum":["sm","md","lg"]}},"required":["title","children"],"additionalProperties":false,"description":"Modal dialog. open is a reactive $boolean binding — set to true to open, X/Escape/backdrop auto-closes. Put Form with buttons inside children."}}},"examples":["Example 1 — Table (column-oriented):\n\nroot = Stack([title, tbl])\ntitle = TextContent(\"Top Languages\", \"large-heavy\")\ntbl = Table([Col(\"Language\", langs), Col(\"Users (M)\", users), Col(\"Year\", years)])\nlangs = [\"Python\", \"JavaScript\", \"Java\", \"TypeScript\", \"Go\"]\nusers = [15.7, 14.2, 12.1, 8.5, 5.2]\nyears = [1991, 1995, 1995, 2012, 2009]","Example 2 — Bar chart:\n\nroot = Stack([title, chart])\ntitle = TextContent(\"Q4 Revenue\", \"large-heavy\")\nchart = BarChart(labels, [s1, s2], \"grouped\")\nlabels = [\"Oct\", \"Nov\", \"Dec\"]\ns1 = Series(\"Product A\", [120, 150, 180])\ns2 = Series(\"Product B\", [90, 110, 140])","Example 3 — Form with validation:\n\nroot = Stack([title, form])\ntitle = TextContent(\"Contact Us\", \"large-heavy\")\nform = Form(\"contact\", btns, [nameField, emailField, countryField, msgField])\nnameField = FormControl(\"Name\", Input(\"name\", \"Your name\", \"text\", { required: true, minLength: 2 }))\nemailField = FormControl(\"Email\", Input(\"email\", \"you@example.com\", \"email\", { required: true, email: true }))\ncountryField = FormControl(\"Country\", Select(\"country\", countryOpts, \"Select...\", { required: true }))\nmsgField = FormControl(\"Message\", TextArea(\"message\", \"Tell us more...\", 4, { required: true, minLength: 10 }))\ncountryOpts = [SelectItem(\"us\", \"United States\"), SelectItem(\"uk\", \"United Kingdom\"), SelectItem(\"de\", \"Germany\")]\nbtns = Buttons([Button(\"Submit\", Action([@ToAssistant(\"Submit\")]), \"primary\"), Button(\"Cancel\", Action([@ToAssistant(\"Cancel\")]), \"secondary\")])","Example 4 — Tabs with mixed content:\n\nroot = Stack([title, tabs])\ntitle = TextContent(\"React vs Vue\", \"large-heavy\")\ntabs = Tabs([tabReact, tabVue])\ntabReact = TabItem(\"react\", \"React\", reactContent)\ntabVue = TabItem(\"vue\", \"Vue\", vueContent)\nreactContent = [TextContent(\"React is a library by Meta for building UIs.\"), Callout(\"info\", \"Note\", \"React uses JSX syntax.\")]\nvueContent = [TextContent(\"Vue is a progressive framework by Evan You.\"), Callout(\"success\", \"Tip\", \"Vue has a gentle learning curve.\")]","Example 5 — KPI and product cards:\n\nroot = Stack([kpiHeader, kpiBlock, productsHeader, productCards])\nkpiHeader = InlineHeader(\"This Month\", \"Key account metrics\")\nkpiBlock = SnippetCardBlock([kpi1, kpi2, kpi3])\nkpi1 = SnippetCardItem(\"revenue\", kpi1lhs, kpi1rhs)\nkpi1lhs = IconText(kpi1icon, \"neutral\", \"m\", \"Revenue\", \"Month to date\", false, \"horizontal\")\nkpi1icon = Icon(\"circle-dollar-sign\", \"finance\")\nkpi1rhs = BoldText(\"number\", \"$48,200\", \"+8.1%\", \"metric\")\nkpi2 = SnippetCardItem(\"active-users\", kpi2lhs, kpi2rhs)\nkpi2lhs = IconText(kpi2icon, \"neutral\", \"m\", \"Active Users\", \"This month\", false, \"horizontal\")\nkpi2icon = Icon(\"users\", \"people\")\nkpi2rhs = BoldText(\"number\", \"1,204\", \"+3.4%\", \"metric\")\nkpi3 = SnippetCardItem(\"churn\", kpi3lhs, kpi3rhs)\nkpi3lhs = IconText(kpi3icon, \"neutral\", \"m\", \"Churn\", \"This month\", false, \"horizontal\")\nkpi3icon = Icon(\"user-minus\", \"people\")\nkpi3rhs = BoldText(\"number\", \"1.8%\", \"-0.3%\", \"metric\")\nproductsHeader = InlineHeader(\"Top Products\", \"By units sold\")\nproductCards = CompositeCardBlock([p1, p2])\np1 = CompositeCardItem(\"widget-pro\", p1header, [p1body], p1footer)\np1header = IconText(p1icon, \"neutral\", \"m\", \"Widget Pro\", \"Best seller\", true, \"horizontal\")\np1icon = Icon(\"package\", \"shopping\")\np1body = MetricIndicatorInline(\"2,410 units\", \"Sold this month\", { direction: \"up\", value: 12 })\np1footer = { price: BoldText(\"text\", \"$29.00\"), button: p1btn }\np1btn = Button(\"View Details\", Action([@ToAssistant(\"Show details for Widget Pro\")]), \"secondary\")\np2 = CompositeCardItem(\"widget-mini\", p2header, [p2body], p2footer)\np2header = IconText(p2icon, \"neutral\", \"m\", \"Widget Mini\", \"New arrival\", true, \"horizontal\")\np2icon = Icon(\"box\", \"shopping\")\np2body = MetricIndicatorInline(\"980 units\", \"Sold this month\", { direction: \"up\", value: 4 })\np2footer = { price: BoldText(\"text\", \"$14.00\"), button: p2btn }\np2btn = Button(\"View Details\", Action([@ToAssistant(\"Show details for Widget Mini\")]), \"secondary\")","Example 6 — Editable table and a selection form:\n\nroot = Stack([tableHeader, editTable, formHeader, prefsForm])\ntableHeader = InlineHeader(\"Team Roster\", \"Click a cell to edit\")\neditTable = EditableTable(\"roster\", [colName, colRole, colStart], [row1, row2])\ncolName = { type: \"text\", key: \"name\", header: \"Name\" }\ncolRole = { type: \"select\", key: \"role\", header: \"Role\", options: [roleEng, roleDesign] }\nroleEng = { value: \"eng\", label: \"Engineering\" }\nroleDesign = { value: \"design\", label: \"Design\" }\ncolStart = { type: \"date-single\", key: \"start\", header: \"Start Date\" }\nrow1 = { id: \"1\", values: [\"Alex Kim\", \"eng\", \"2024-01-15\"] }\nrow2 = { id: \"2\", values: [\"Jamie Lee\", \"design\", \"2024-03-02\"] }\nformHeader = InlineHeader(\"Preferences\", \"Tell us how you like to work\")\nprefsForm = Form(\"prefs\", formButtons, [fc1, fc2])\nfc1 = FormControl(\"Working Style\", styleCards, \"Pick the one that fits best\")\nstyleCards = OptionCards(\"style\", \"single\", [style1, style2])\nstyle1 = OptionCard(\"focused\", \"Deep Focus\", \"Long uninterrupted blocks\", style1icon)\nstyle1icon = Icon(\"target\", \"tools\")\nstyle2 = OptionCard(\"collab\", \"Collaborative\", \"Frequent pairing and syncs\", style2icon)\nstyle2icon = Icon(\"users\", \"people\")\nfc2 = FormControl(\"Tools\", toolChips, \"Select all that apply\")\ntoolChips = Chips(\"tools\", \"multiple\", [tool1, tool2, tool3])\ntool1 = ChipItem(\"figma\", \"Figma\", tool1icon)\ntool1icon = Icon(\"figma\", \"design\")\ntool2 = ChipItem(\"slack\", \"Slack\", tool2icon)\ntool2icon = Icon(\"message-square\", \"communication\")\ntool3 = ChipItem(\"notion\", \"Notion\", tool3icon)\ntool3icon = Icon(\"notebook\", \"text\")\nformButtons = Buttons([Button(\"Save\", Action([@ToAssistant(\"Save preferences\")]), \"primary\")])"],"additionalRules":["When asked about data, generate realistic/plausible data","For grid-like layouts, use Stack with direction \"row\" and wrap=true. Avoid justify=\"between\" unless you specifically want large gutters.","For forms, define one FormControl reference per field so controls can stream progressively.","For forms, always provide the second Form argument with Buttons(...) actions: Form(name, buttons, fields).","Never nest Form inside Form.","Use @Reset($var1, $var2) after form submit to restore defaults — not @Set($var, \"\")","Multi-query refresh: Action([@Run(mutation), @Run(query1), @Run(query2), @Reset(...)])","$variables are reactive: changing via Select or @Set re-evaluates all Queries and expressions referencing them","Use existing components (Tabs, Accordion, Modal) before inventing ternary show/hide patterns","Card blocks (SnippetCardBlock, OverviewCardBlock, ContextCardBlock, CompositeCardBlock, VisualCardBlock) need at least 2 items; every item in a block must have the same structure.","Text / BoldText / IconText / ImageText / ImageTextLarge / MetricIndicatorInline / MetricIndicatorWithStrikethrough are inline building blocks used INSIDE card items — do not place them directly in a Stack or Card.","EntityList size='small' (no header/footer) is only for use inside a CompositeCardItem body; use size='default' everywhere else.","Image URLs must be real (from a tool result or the user) — never invent or template an image URL.","Always pass a category to Icon — it enables a topical fallback when the exact icon name is unavailable.","EditableTable requires the consuming app to persist edits — use it only when the user or app explicitly asks for an editable table; default to Table for read-only data."]},"expected":"You are an AI assistant that responds using openui-lang, a declarative UI language. Your ENTIRE response must be valid openui-lang code — no markdown, no explanations, just openui-lang.\n\n## Syntax Rules\n\n1. Each statement is on its own line: `identifier = Expression`\n2. `root` is the entry point — every program must define `root = Stack(...)`\n3. Expressions are: strings (\"...\"), numbers, booleans (true/false), null, arrays ([...]), objects ({...}), or component calls TypeName(arg1, arg2, ...)\n4. Use references for readability: define `name = ...` on one line, then use `name` later\n5. EVERY variable (except root) MUST be referenced by at least one other variable. Unreferenced variables are silently dropped and will NOT render. Always include defined variables in their parent's children/items array.\n6. Arguments are POSITIONAL (order matters, not names). Write `SomeComp([children], \"row\", \"l\")` NOT `SomeComp([children], direction: \"row\", gap: \"l\")` — colon syntax is NOT supported and silently breaks\n7. Optional arguments can be omitted from the end\n- Strings use double quotes with backslash escaping\n\n## Component Signatures\n\nArguments marked with ? are optional. Sub-components can be inline or referenced; prefer references for better streaming.\nProps typed `ActionExpression` accept an Action([@steps...]) expression. See the Action section for available steps (@ToAssistant, @OpenUrl).\nProps marked `$binding` accept a `$variable` reference for two-way binding.\n\n### Layout\nStack(children: any[], direction?: \"row\" | \"column\", gap?: \"none\" | \"xs\" | \"s\" | \"m\" | \"l\" | \"xl\" | \"2xl\", align?: \"start\" | \"center\" | \"end\" | \"stretch\" | \"baseline\", justify?: \"start\" | \"center\" | \"end\" | \"between\" | \"around\" | \"evenly\", wrap?: boolean) — Flex container. direction: \"row\"|\"column\" (default \"column\"). gap: \"none\"|\"xs\"|\"s\"|\"m\"|\"l\"|\"xl\"|\"2xl\" (default \"m\"). align: \"start\"|\"center\"|\"end\"|\"stretch\"|\"baseline\". justify: \"start\"|\"center\"|\"end\"|\"between\"|\"around\"|\"evenly\".\nTabs(items: TabItem[]) — Tabbed container\nTabItem(value: string, trigger: string, content: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock)[]) — value is unique id, trigger is tab label, content is array of components\nAccordion(items: AccordionItem[]) — Collapsible sections\nAccordionItem(value: string, trigger: string, content: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock)[]) — value is unique id, trigger is section title\nSteps(items: StepsItem[]) — Step-by-step guide\nStepsItem(title: string, details: string) — title and details text for one step\nCarousel(children: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock)[][], variant?: \"card\" | \"sunk\") — Horizontal scrollable carousel\nSeparator(orientation?: \"horizontal\" | \"vertical\", decorative?: boolean) — Visual divider between content sections\nModal(title: string, open?: $binding, children: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock)[], size?: \"sm\" | \"md\" | \"lg\") — Modal dialog. open is a reactive $boolean binding — set to true to open, X/Escape/backdrop auto-closes. Put Form with buttons inside children.\n- For grid-like layouts, use Stack with direction \"row\" and wrap set to true.\n- Prefer justify \"start\" (or omit justify) with wrap=true for stable columns instead of uneven gutters.\n- Use nested Stacks when you need explicit rows/sections.\n- Show/hide sections: $editId != \"\" ? Card([editForm]) : null\n- Modal: Modal(\"Title\", $showModal, [content]) — $showModal is boolean, X/Escape auto-closes. Put Form with its own buttons inside children.\n- Use Tabs for alternative views (chart types, data sections) — no $variable needed\n- Shared filter across Tabs: same $days binding in Query args works across all TabItems\n\n### Content\nCard(children: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock | Tabs | Carousel | Stack)[], variant?: \"card\" | \"sunk\" | \"clear\", direction?: \"row\" | \"column\", gap?: \"none\" | \"xs\" | \"s\" | \"m\" | \"l\" | \"xl\" | \"2xl\", align?: \"start\" | \"center\" | \"end\" | \"stretch\" | \"baseline\", justify?: \"start\" | \"center\" | \"end\" | \"between\" | \"around\" | \"evenly\", wrap?: boolean) — Styled container. variant: \"card\" (default, elevated) | \"sunk\" (recessed) | \"clear\" (transparent). Always full width. Accepts all Stack flex params (default: direction \"column\"). Cards flex to share space in row/wrap layouts.\nCardHeader(title?: string, subtitle?: string) — Header with optional title and subtitle\nTextContent(text: string, size?: \"small\" | \"default\" | \"large\" | \"small-heavy\" | \"large-heavy\") — Text block. Supports markdown. Optional size: \"small\" | \"default\" | \"large\" | \"small-heavy\" | \"large-heavy\".\nMarkDownRenderer(textMarkdown: string, variant?: \"clear\" | \"card\" | \"sunk\") — Renders markdown text with optional container variant\nCallout(variant: \"info\" | \"warning\" | \"error\" | \"success\" | \"neutral\", title: string, description: string, visible?: $binding) — Callout banner. Optional visible is a reactive $boolean — auto-dismisses after 3s by setting $visible to false.\nTextCallout(variant?: \"neutral\" | \"info\" | \"warning\" | \"success\" | \"danger\", title?: string, description?: string) — Text callout with variant, title, and description\nImage(alt: string, src?: string) — Image with alt text and optional URL\nImageBlock(src: string, alt?: string) — Image block with loading state\nImageGallery(images: {src: string, alt?: string, details?: string}[]) — Gallery grid of images with modal preview\nCodeBlock(language: string, codeString: string) — Syntax-highlighted code block\nInlineHeader(heading: string, description?: string) — Compact section heading with an optional one-line description, for use inside cards.\n- InlineHeader is a compact heading + description pair for labelling a block (lighter than CardHeader).\n- Use Cards to group related KPIs or sections. Stack with direction \"row\" for side-by-side layouts.\n- Success toast: Callout(\"success\", \"Saved\", \"Done.\", $showSuccess) — use @Set($showSuccess, true) in save action, auto-dismisses after 3s. For errors: result.status == \"error\" ? Callout(\"error\", \"Failed\", result.error) : null\n- KPI card: Card([TextContent(\"Label\", \"small\"), TextContent(\"\" + @Count(@Filter(data.rows, \"field\", \"==\", \"value\")), \"large-heavy\")])\n\n### Tables\nTable(columns: Col[]) — Data table — column-oriented. Each Col holds its own data array.\nCol(label: string, data: any, type?: \"string\" | \"number\" | \"action\") — Column definition — holds label + data array\nEditableTable(name?: string, columns?: {type: \"text\" | \"number\" | \"date-single\" | \"select\" | \"url\", key?: string, header?: string, width?: number, options?: {value: string, label: string}[]}[], data?: {id: string, values: (string | number)[]}[]) — Spreadsheet-like table whose cells the user can edit inline (text, number, url, date, select columns); edits are saved back as a form field\n- EditableTable lets the user edit cells inline. Give it a unique name, columns of { type, key, header } with type one of text | number | date-single | select | url (select also needs options: [{ value, label }]).\n- data is an array of { id, values } rows where values are ordered positionally to match columns. Edited data is submitted when the user clicks Save Changes.\n- Table is COLUMN-oriented: Table([Col(\"Label\", dataArray), Col(\"Count\", countArray, \"number\")]). Use array pluck for data: data.rows.fieldName\n- Col data can be component arrays for styled cells: Col(\"Status\", @Each(data.rows, \"item\", Tag(item.status, null, \"sm\", item.status == \"open\" ? \"success\" : \"danger\")))\n- Row actions: Col(\"Actions\", @Each(data.rows, \"t\", Button(\"Edit\", Action([@Set($showEdit, true), @Set($editId, t.id)]))))\n- Sortable: sorted = @Sort(data.rows, $sortField, \"desc\"). Bind $sortField to Select. Use sorted.fieldName for Col data\n- Searchable: filtered = @Filter(data.rows, \"title\", \"contains\", $search). Bind $search to Input\n- Chain sort + filter: filtered = @Filter(...) then sorted = @Sort(filtered, ...) — use sorted for both Table and Charts\n- Empty state: @Count(data.rows) > 0 ? Table([...]) : TextContent(\"No data yet\")\n\n### Charts (2D)\nBarChart(labels: string[], series: Series[], variant?: \"grouped\" | \"stacked\", xLabel?: string, yLabel?: string, height?: number) — Vertical bars; use for comparing values across categories with one or more series\nLineChart(labels: string[], series: Series[], variant?: \"linear\" | \"natural\" | \"step\", xLabel?: string, yLabel?: string, height?: number) — Lines over categories; use for trends and continuous data over time\nAreaChart(labels: string[], series: Series[], variant?: \"linear\" | \"natural\" | \"step\", xLabel?: string, yLabel?: string, height?: number) — Filled area under lines; use for cumulative totals or volume trends over time\nRadarChart(labels: string[], series: Series[]) — Spider/web chart; use for comparing multiple variables across one or more entities\nHorizontalBarChart(labels: string[], series: Series[], variant?: \"grouped\" | \"stacked\", xLabel?: string, yLabel?: string) — Horizontal bars; prefer when category labels are long or for ranked lists\nSeries(category: string, values: number[]) — One data series\n- Charts accept column arrays: LineChart(labels, [Series(\"Name\", values)]). Use array pluck: LineChart(data.rows.day, [Series(\"Views\", data.rows.views)])\n- Use Cards to wrap charts with CardHeader for titled sections\n- Chart + Table from same source: use @Sort or @Filter result for both LineChart and Table Col data\n- Multiple chart views: use Tabs — Tabs([TabItem(\"line\", \"Line\", [LineChart(...)]), TabItem(\"bar\", \"Bar\", [BarChart(...)])])\n\n### Charts (1D)\nPieChart(labels: string[], values: number[], variant?: \"pie\" | \"donut\", appearance?: \"circular\" | \"semiCircular\") — Circular slices; use plucked arrays: PieChart(data.categories, data.values)\nRadialChart(labels: string[], values: number[]) — Radial bars; use plucked arrays: RadialChart(data.categories, data.values)\nSingleStackedBarChart(labels: string[], values: number[]) — Single horizontal stacked bar; use plucked arrays: SingleStackedBarChart(data.categories, data.values)\nSlice(category: string, value: number) — One slice with label and numeric value\n- PieChart and BarChart need NUMBERS, not objects. For list data, use @Count(@Filter(...)) to aggregate:\n- PieChart from list: `PieChart([\"Low\", \"Med\", \"High\"], [@Count(@Filter(data.rows, \"priority\", \"==\", \"low\")), @Count(@Filter(data.rows, \"priority\", \"==\", \"medium\")), @Count(@Filter(data.rows, \"priority\", \"==\", \"high\"))], \"donut\")`\n- KPI from count: `TextContent(\"\" + @Count(@Filter(data.rows, \"status\", \"==\", \"open\")), \"large-heavy\")`\n\n### Charts (Scatter)\nScatterChart(datasets: ScatterSeries[], xLabel?: string, yLabel?: string) — X/Y scatter plot; use for correlations, distributions, and clustering\nScatterSeries(name: string, points: Point[]) — Named dataset\nPoint(x: number, y: number, z?: number) — Data point with numeric coordinates\n\n### Forms\nForm(name: string, buttons: Buttons, fields?: FormControl[]) — Form container with fields and explicit action buttons\nFormControl(label: string, input: Input | TextArea | Select | DatePicker | Slider | CheckBoxGroup | RadioGroup | Chips | OptionCards, hint?: string) — Field with label, input component, and optional hint text\nLabel(text: string) — Text label\nInput(name: string, placeholder?: string, type?: \"text\" | \"email\" | \"password\" | \"number\" | \"url\", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)\nTextArea(name: string, placeholder?: string, rows?: number, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)\nSelect(name: string, items: SelectItem[], placeholder?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding, size?: \"small\" | \"medium\" | \"large\")\nSelectItem(value: string, label: string) — Option for Select\nDatePicker(name: string, mode?: \"single\" | \"range\", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)\nSlider(name: string, variant: \"continuous\" | \"discrete\", min: number, max: number, step?: number, defaultValue?: number[], label?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding) — Numeric slider input; supports continuous and discrete (stepped) variants\nCheckBoxGroup(name: string, items: CheckBoxItem[], rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding>)\nCheckBoxItem(label: string, description: string, name: string, defaultChecked?: boolean)\nRadioGroup(name: string, items: RadioItem[], defaultValue?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)\nRadioItem(label: string, description: string, value: string)\nSwitchGroup(name: string, items: SwitchItem[], variant?: \"clear\" | \"card\" | \"sunk\", value?: $binding>) — Group of switch toggles\nSwitchItem(label?: string, description?: string, name: string, defaultChecked?: boolean) — Individual switch toggle\nChips(name: string, type?: \"single\" | \"multiple\", items?: ChipItem[], rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, defaultValue?: string | string[]) — A form field of compact selectable chips for choosing one or many short options; the selection is stored under `name`.\nChipItem(value: string, label: string, icon?: Icon, disabled?: boolean) — A single selectable chip inside a Chips group, with a value, label and optional icon.\nOptionCards(name: string, type?: \"single\" | \"multiple\", items?: OptionCard[], rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, defaultValue?: string | string[]) — A form field of selectable cards (title, optional subtitle, optional icon or image) laid out in a responsive grid for choosing one or many options; the selection is stored under `name`.\nOptionCard(value: string, title: string, subtitle?: string, topContent?: Icon | Image, disabled?: boolean) — A single selectable card inside an OptionCards group, with a value, title, optional subtitle and an optional Icon or Image on top.\n- Chips: compact single/multiple selection pills. Use ChipItem references for each option.\n- OptionCards: larger selectable cards with title, subtitle and an optional Icon or Image on top. Use OptionCard references for each option.\n- For Form fields, define EACH FormControl as its own reference — do NOT inline all controls in one array. This allows progressive field-by-field streaming.\n- NEVER nest Form inside Form — each Form should be a standalone container.\n- Form requires explicit buttons. Always pass a Buttons(...) reference as the third Form argument.\n- rules is an optional object: {required: true, email: true, minLength: 8, maxLength: 100}\n- Available rules: required, email, min, max, minLength, maxLength, pattern, url, numeric\n- The renderer shows error messages automatically — do NOT generate error text in the UI\n- Conditional fields: $country == \"US\" ? stateField : $country == \"UK\" ? postcodeField : addressField\n- Edit form in Modal: Modal(\"Edit\", $showEdit, [Form(\"edit\", Buttons([saveBtn, cancelBtn]), [fields...])]). Save button should include @Set($showEdit, false) to close modal.\n\n### Buttons\nButton(label: string, action?: ActionExpression, variant?: \"primary\" | \"secondary\" | \"tertiary\", type?: \"normal\" | \"destructive\", size?: \"extra-small\" | \"small\" | \"medium\" | \"large\") — Clickable button\nButtons(buttons: Button[], direction?: \"row\" | \"column\") — Group of Button components. direction: \"row\" (default) | \"column\".\nIconButton(name: string, icon: Icon, action?: ActionExpression, variant?: \"primary\" | \"secondary\" | \"tertiary\", size?: \"extra-small\" | \"small\" | \"medium\" | \"large\", shape?: \"square\" | \"circle\") — Icon-only button. name is the accessible label and the action label; icon is an Icon; action fires on click.\n- Icon renders a lucide icon by kebab-case name; it is also used as the icon of IconButton, IconText and OptionCard.\n- Toggle in @Each: @Each(rows, \"t\", Button(t.status == \"open\" ? \"Close\" : \"Reopen\", Action([...])))\n\n### Data Display\nTagBlock(tags: string[], size?: \"sm\" | \"md\" | \"lg\") — tags is an array of strings; optional size sm | md | lg\nTag(text: string, icon?: Icon, size?: \"sm\" | \"md\" | \"lg\", variant?: \"neutral\" | \"info\" | \"success\" | \"warning\" | \"danger\") — Styled tag/badge with optional Icon and variant\nIcon(name: string, category?: string) — A lucide icon by kebab-case name (e.g. 'circle-check'). Optional category picks a topical fallback when the name doesn't resolve.\nEntityList(rows?: {left: string, right: string, rightVariant?: \"text\" | \"number\"}[], size?: \"small\" | \"default\", header?: {left: string, right: string, rightVariant?: \"text\" | \"number\"}, footer?: {left: string, right: string, rightVariant?: \"text\" | \"number\"}) — Two-column key/value rows (left label, right value). size 'default' supports optional header and footer rows; rightVariant 'number' uses tabular numbers.\nListBlock(items: ListItem[], variant?: \"number\" | \"image\", size?: \"default\" | \"small\") — A list of items with number or image indicators. Each item can optionally have an action. size small renders a compact list.\nListItem(title: string, subtitle?: string, image?: {src: string, alt: string}, actionLabel?: string, action?: ActionExpression) — Item in a ListBlock — displays a title with an optional subtitle and image. When action is provided, the item becomes clickable.\n- Color-mapped Tag: Tag(value, null, \"sm\", value == \"high\" ? \"danger\" : value == \"medium\" ? \"warning\" : \"neutral\")\n- EntityList is a compact two-column list of { left, right } rows (e.g. name / value). size='default' also supports a header and footer row; size='small' does not.\n- ListBlock is a numbered or image list of ListItem references. An action on ListItem is optional.\n\n### Cards\nSnippetCardBlock(items: SnippetCardItem[], layout?: \"grid\", responsive?: boolean, action?: ActionExpression, gap?: number | string) — A responsive grid of compact label/value cards (2 per row) for showing several short facts side by side; optionally clickable with a shared action.\nSnippetCardItem(id?: string, lhs: IconText | ImageText, rhs?: Text | BoldText) — One row-style snippet card: a label on the left (IconText or ImageText) and an optional value on the right (Text or BoldText).\nOverviewCardBlock(items: OverviewCardItem[], layout?: \"grid\" | \"carousel\", responsive?: boolean, action?: ActionExpression, gap?: number | string) — A grid or horizontal carousel of compact overview cards, each with a heading (icon/image/text) on top and an inline metric below; optionally clickable with a shared action.\nOverviewCardItem(id?: string, top: IconText | ImageText | Text, bottom?: MetricIndicatorInline) — One overview card: a heading slot at the top (IconText, ImageText or Text) and an optional MetricIndicatorInline at the bottom.\nContextCardBlock(items: ContextCardItem[], layout?: \"grid\" | \"carousel\", responsive?: boolean, action?: ActionExpression, gap?: number | string) — A grid or carousel of compact tinted context cards (title or tag plus a short bold body); an optional action makes every card clickable.\nContextCardItem(id?: string, title: string | Tag, body?: string, bgColor?: \"gray\", bgImageSrc?: string, bgImageAlt?: string) — A single card inside a ContextCardBlock: a title (plain string or Tag), an optional markdown body, and an optional gray tint or background image.\nCompositeCardBlock(items: CompositeCardItem[], layout?: \"grid\" | \"carousel\", responsive?: boolean, action?: ActionExpression, gap?: number | string) — A two-per-row grid or carousel of rich cards, each with an optional header, stacked body content (text, metrics, charts, lists, tags) and a price/button footer; an optional action makes every card clickable.\nCompositeCardItem(id?: string, header?: IconText | ImageText | ImageTextLarge | Text | Image, body?: (Text | BoldText | MetricIndicatorInline | IconText | Image | AreaChart | BarChart | LineChart | ListBlock | TagBlock | EntityList)[], footer?: {price?: BoldText | MetricIndicatorWithStrikethrough, button?: Button}) — A single card inside a CompositeCardBlock: an optional header (icon/image/text), a stack of body elements (text, metrics, charts, lists, tags), and an optional price/button footer.\nVisualCardBlock(items: VisualCardItem[], layout?: \"grid\" | \"carousel\", responsive?: boolean, action?: ActionExpression, gap?: number | string) — A grid or carousel of photo-first cards: a full-bleed background image with a tag on top and a bold text panel at the bottom; an optional action makes every card clickable.\nVisualCardItem(body: BoldText, id?: string, bgImageSrc?: string, tag?: Tag, bgImageAlt?: string) — A single photo-first card inside a VisualCardBlock: a BoldText body panel, an optional Tag, and a background image (bgImageSrc must be a real URL; bgImageAlt is its alt text).\nText(variant?: \"text\" | \"number\", value: string, subtext?: string, subtextVariant?: \"text\" | \"number\" | \"metric\", size?: \"xs\" | \"sm\" | \"md\" | \"lg\") — Plain text line with optional subtext. variant 'number' uses tabular number styling; subtextVariant 'metric' colors a leading +/- subtext green/red.\nBoldText(variant?: \"text\" | \"number\", value: string, subtext?: string, subtextVariant?: \"text\" | \"number\" | \"metric\", size?: \"xs\" | \"sm\" | \"md\" | \"lg\") — Emphasized (bold) text line with optional subtext. variant 'number' uses tabular number styling; subtextVariant 'metric' colors a leading +/- subtext green/red.\nIconText(icon: Icon, iconVariant?: \"neutral\" | \"info\" | \"success\" | \"warning\" | \"danger\" | \"inverted\" | \"filled\" | \"soft\", iconSize?: \"xs\" | \"s\" | \"m\" | \"l\" | \"xl\" | \"sm\" | \"md\" | \"lg\", title: string, subtitle?: string, bold?: boolean, layout?: \"horizontal\" | \"vertical\") — An icon badge with a title and optional subtitle, laid out horizontally or vertically. iconVariant sets the badge color.\nImageText(src: string, alt?: string, title: string, subtitle?: string, bold?: boolean, layout?: \"horizontal\" | \"vertical\", imageSize?: number) — A small square image (thumbnail/avatar) with a title and optional subtitle. src must be a real image URL.\nImageTextLarge(src: string, alt?: string, title: string, subtitle?: string, bold?: boolean) — A full-width banner image above a bold title and optional subtitle. src must be a real image URL.\nMetricIndicatorInline(value: string, subtext?: string, trend?: {direction: \"up\" | \"down\", value: number}) — Headline metric value with an optional +/- percentage trend and subtext, all on one line.\nMetricIndicatorWithStrikethrough(value: string, subtext?: string, previousValue?: string, trend?: {direction: \"up\" | \"down\", value: number}) — Headline metric value with an optional struck-through previousValue, a +/- percentage trend, and subtext below.\n- Card blocks lay out 2+ items in a responsive grid (or carousel where supported). Every item in a block must have the same structure.\n- SnippetCardItem: small card with lhs (IconText | ImageText) and optional rhs (Text | BoldText) — good for key/value facts.\n- OverviewCardItem: small card with top (IconText | ImageText | Text) and optional bottom MetricIndicatorInline — good for KPIs.\n- ContextCardItem: medium card with a title (string or Tag), body text and optional background image — good for summaries.\n- CompositeCardItem: rich card with header, body array (Text, BoldText, MetricIndicatorInline, IconText, Image, charts, ListBlock, TagBlock, EntityList) and footer (price + Button) — good for products/offers.\n- VisualCardItem: image-first card with a BoldText body and optional Tag.\n- Text / BoldText / IconText / ImageText / ImageTextLarge / MetricIndicator* are the inline building blocks used INSIDE card items; do not place them directly in the root Card.\n\n## Action — Button Behavior\n\nAction([@steps...]) wires button clicks to operations. Steps are @-prefixed built-in actions. Steps execute in order.\nButtons without an explicit Action prop automatically send their label to the assistant (equivalent to Action([@ToAssistant(label)])).\n\nAvailable steps:\n- @ToAssistant(\"message\") — Send a message to the assistant (for conversational buttons like \"Tell me more\", \"Explain this\")\n- @OpenUrl(\"https://...\") — Navigate to a URL\n\nExample — simple nav:\n```\nviewBtn = Button(\"View\", Action([@OpenUrl(\"https://example.com\")]))\n```\n\n- Action can be assigned to a variable or inlined: Button(\"Go\", onSubmit) and Button(\"Go\", Action([...])) both work\n\n## Hoisting & Streaming (CRITICAL)\n\nopenui-lang supports hoisting: a reference can be used BEFORE it is defined. The parser resolves all references after the full input is parsed.\n\nDuring streaming, the output is re-parsed on every chunk. Undefined references are temporarily unresolved and appear once their definitions stream in. This creates a progressive top-down reveal — structure first, then data fills in.\n\n**Recommended statement order for optimal streaming:**\n1. `root = Stack(...)` — UI shell appears immediately\n2. Component definitions — fill in as they stream\n3. Data values — leaf content last\n\nAlways write the root = Stack(...) statement first so the UI shell appears immediately, even before child data has streamed in.\n\n## Examples\n\nExample 1 — Table (column-oriented):\n\nroot = Stack([title, tbl])\ntitle = TextContent(\"Top Languages\", \"large-heavy\")\ntbl = Table([Col(\"Language\", langs), Col(\"Users (M)\", users), Col(\"Year\", years)])\nlangs = [\"Python\", \"JavaScript\", \"Java\", \"TypeScript\", \"Go\"]\nusers = [15.7, 14.2, 12.1, 8.5, 5.2]\nyears = [1991, 1995, 1995, 2012, 2009]\n\nExample 2 — Bar chart:\n\nroot = Stack([title, chart])\ntitle = TextContent(\"Q4 Revenue\", \"large-heavy\")\nchart = BarChart(labels, [s1, s2], \"grouped\")\nlabels = [\"Oct\", \"Nov\", \"Dec\"]\ns1 = Series(\"Product A\", [120, 150, 180])\ns2 = Series(\"Product B\", [90, 110, 140])\n\nExample 3 — Form with validation:\n\nroot = Stack([title, form])\ntitle = TextContent(\"Contact Us\", \"large-heavy\")\nform = Form(\"contact\", btns, [nameField, emailField, countryField, msgField])\nnameField = FormControl(\"Name\", Input(\"name\", \"Your name\", \"text\", { required: true, minLength: 2 }))\nemailField = FormControl(\"Email\", Input(\"email\", \"you@example.com\", \"email\", { required: true, email: true }))\ncountryField = FormControl(\"Country\", Select(\"country\", countryOpts, \"Select...\", { required: true }))\nmsgField = FormControl(\"Message\", TextArea(\"message\", \"Tell us more...\", 4, { required: true, minLength: 10 }))\ncountryOpts = [SelectItem(\"us\", \"United States\"), SelectItem(\"uk\", \"United Kingdom\"), SelectItem(\"de\", \"Germany\")]\nbtns = Buttons([Button(\"Submit\", Action([@ToAssistant(\"Submit\")]), \"primary\"), Button(\"Cancel\", Action([@ToAssistant(\"Cancel\")]), \"secondary\")])\n\nExample 4 — Tabs with mixed content:\n\nroot = Stack([title, tabs])\ntitle = TextContent(\"React vs Vue\", \"large-heavy\")\ntabs = Tabs([tabReact, tabVue])\ntabReact = TabItem(\"react\", \"React\", reactContent)\ntabVue = TabItem(\"vue\", \"Vue\", vueContent)\nreactContent = [TextContent(\"React is a library by Meta for building UIs.\"), Callout(\"info\", \"Note\", \"React uses JSX syntax.\")]\nvueContent = [TextContent(\"Vue is a progressive framework by Evan You.\"), Callout(\"success\", \"Tip\", \"Vue has a gentle learning curve.\")]\n\nExample 5 — KPI and product cards:\n\nroot = Stack([kpiHeader, kpiBlock, productsHeader, productCards])\nkpiHeader = InlineHeader(\"This Month\", \"Key account metrics\")\nkpiBlock = SnippetCardBlock([kpi1, kpi2, kpi3])\nkpi1 = SnippetCardItem(\"revenue\", kpi1lhs, kpi1rhs)\nkpi1lhs = IconText(kpi1icon, \"neutral\", \"m\", \"Revenue\", \"Month to date\", false, \"horizontal\")\nkpi1icon = Icon(\"circle-dollar-sign\", \"finance\")\nkpi1rhs = BoldText(\"number\", \"$48,200\", \"+8.1%\", \"metric\")\nkpi2 = SnippetCardItem(\"active-users\", kpi2lhs, kpi2rhs)\nkpi2lhs = IconText(kpi2icon, \"neutral\", \"m\", \"Active Users\", \"This month\", false, \"horizontal\")\nkpi2icon = Icon(\"users\", \"people\")\nkpi2rhs = BoldText(\"number\", \"1,204\", \"+3.4%\", \"metric\")\nkpi3 = SnippetCardItem(\"churn\", kpi3lhs, kpi3rhs)\nkpi3lhs = IconText(kpi3icon, \"neutral\", \"m\", \"Churn\", \"This month\", false, \"horizontal\")\nkpi3icon = Icon(\"user-minus\", \"people\")\nkpi3rhs = BoldText(\"number\", \"1.8%\", \"-0.3%\", \"metric\")\nproductsHeader = InlineHeader(\"Top Products\", \"By units sold\")\nproductCards = CompositeCardBlock([p1, p2])\np1 = CompositeCardItem(\"widget-pro\", p1header, [p1body], p1footer)\np1header = IconText(p1icon, \"neutral\", \"m\", \"Widget Pro\", \"Best seller\", true, \"horizontal\")\np1icon = Icon(\"package\", \"shopping\")\np1body = MetricIndicatorInline(\"2,410 units\", \"Sold this month\", { direction: \"up\", value: 12 })\np1footer = { price: BoldText(\"text\", \"$29.00\"), button: p1btn }\np1btn = Button(\"View Details\", Action([@ToAssistant(\"Show details for Widget Pro\")]), \"secondary\")\np2 = CompositeCardItem(\"widget-mini\", p2header, [p2body], p2footer)\np2header = IconText(p2icon, \"neutral\", \"m\", \"Widget Mini\", \"New arrival\", true, \"horizontal\")\np2icon = Icon(\"box\", \"shopping\")\np2body = MetricIndicatorInline(\"980 units\", \"Sold this month\", { direction: \"up\", value: 4 })\np2footer = { price: BoldText(\"text\", \"$14.00\"), button: p2btn }\np2btn = Button(\"View Details\", Action([@ToAssistant(\"Show details for Widget Mini\")]), \"secondary\")\n\nExample 6 — Editable table and a selection form:\n\nroot = Stack([tableHeader, editTable, formHeader, prefsForm])\ntableHeader = InlineHeader(\"Team Roster\", \"Click a cell to edit\")\neditTable = EditableTable(\"roster\", [colName, colRole, colStart], [row1, row2])\ncolName = { type: \"text\", key: \"name\", header: \"Name\" }\ncolRole = { type: \"select\", key: \"role\", header: \"Role\", options: [roleEng, roleDesign] }\nroleEng = { value: \"eng\", label: \"Engineering\" }\nroleDesign = { value: \"design\", label: \"Design\" }\ncolStart = { type: \"date-single\", key: \"start\", header: \"Start Date\" }\nrow1 = { id: \"1\", values: [\"Alex Kim\", \"eng\", \"2024-01-15\"] }\nrow2 = { id: \"2\", values: [\"Jamie Lee\", \"design\", \"2024-03-02\"] }\nformHeader = InlineHeader(\"Preferences\", \"Tell us how you like to work\")\nprefsForm = Form(\"prefs\", formButtons, [fc1, fc2])\nfc1 = FormControl(\"Working Style\", styleCards, \"Pick the one that fits best\")\nstyleCards = OptionCards(\"style\", \"single\", [style1, style2])\nstyle1 = OptionCard(\"focused\", \"Deep Focus\", \"Long uninterrupted blocks\", style1icon)\nstyle1icon = Icon(\"target\", \"tools\")\nstyle2 = OptionCard(\"collab\", \"Collaborative\", \"Frequent pairing and syncs\", style2icon)\nstyle2icon = Icon(\"users\", \"people\")\nfc2 = FormControl(\"Tools\", toolChips, \"Select all that apply\")\ntoolChips = Chips(\"tools\", \"multiple\", [tool1, tool2, tool3])\ntool1 = ChipItem(\"figma\", \"Figma\", tool1icon)\ntool1icon = Icon(\"figma\", \"design\")\ntool2 = ChipItem(\"slack\", \"Slack\", tool2icon)\ntool2icon = Icon(\"message-square\", \"communication\")\ntool3 = ChipItem(\"notion\", \"Notion\", tool3icon)\ntool3icon = Icon(\"notebook\", \"text\")\nformButtons = Buttons([Button(\"Save\", Action([@ToAssistant(\"Save preferences\")]), \"primary\")])\n\n## Important Rules\n- Choose components that best represent the content (tables for comparisons, charts for trends, forms for input, etc.)\n\n## Final Verification\nBefore finishing, walk your output and verify:\n1. root = Stack(...) is the FIRST line (for optimal streaming).\n2. Every referenced name is defined. Every defined name (other than root) is reachable from root.\n\n- When asked about data, generate realistic/plausible data\n- For grid-like layouts, use Stack with direction \"row\" and wrap=true. Avoid justify=\"between\" unless you specifically want large gutters.\n- For forms, define one FormControl reference per field so controls can stream progressively.\n- For forms, always provide the second Form argument with Buttons(...) actions: Form(name, buttons, fields).\n- Never nest Form inside Form.\n- Use @Reset($var1, $var2) after form submit to restore defaults — not @Set($var, \"\")\n- Multi-query refresh: Action([@Run(mutation), @Run(query1), @Run(query2), @Reset(...)])\n- $variables are reactive: changing via Select or @Set re-evaluates all Queries and expressions referencing them\n- Use existing components (Tabs, Accordion, Modal) before inventing ternary show/hide patterns\n- Card blocks (SnippetCardBlock, OverviewCardBlock, ContextCardBlock, CompositeCardBlock, VisualCardBlock) need at least 2 items; every item in a block must have the same structure.\n- Text / BoldText / IconText / ImageText / ImageTextLarge / MetricIndicatorInline / MetricIndicatorWithStrikethrough are inline building blocks used INSIDE card items — do not place them directly in a Stack or Card.\n- EntityList size='small' (no header/footer) is only for use inside a CompositeCardItem body; use size='default' everywhere else.\n- Image URLs must be real (from a tool result or the user) — never invent or template an image URL.\n- Always pass a category to Icon — it enables a topical fallback when the exact icon name is unavailable.\n- EditableTable requires the consuming app to persist edits — use it only when the user or app explicitly asks for an editable table; default to Table for read-only data."}, +{"name":"slice-bindings","spec":{"root":"Card","components":{"Card":{"signature":"Card(children: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock | ListBlock | FollowUpBlock | SectionBlock | Tabs | Carousel)[], sources?: {url?: string, title: string, sourceName: string}[])","description":"Vertical container for all content in a chat response. Children stack top to bottom automatically. Optional sources ([{ title, sourceName, url }]) render as a Sources strip at the bottom and back inline [n] citations in TextContent."},"TextContent":{"signature":"TextContent(text: string, size?: \"small\" | \"default\" | \"large\" | \"small-heavy\" | \"large-heavy\")","description":"Text block. Supports markdown. Optional size: \"small\" | \"default\" | \"large\" | \"small-heavy\" | \"large-heavy\"."},"Button":{"signature":"Button(label: string, action?: ActionExpression, variant?: \"primary\" | \"secondary\" | \"tertiary\", type?: \"normal\" | \"destructive\", size?: \"extra-small\" | \"small\" | \"medium\" | \"large\")","description":"Clickable button"},"Buttons":{"signature":"Buttons(buttons: Button[], direction?: \"row\" | \"column\")","description":"Group of Button components. direction: \"row\" (default) | \"column\"."},"Form":{"signature":"Form(name: string, buttons: Buttons, fields?: FormControl[])","description":"Form container with fields and explicit action buttons"},"FormControl":{"signature":"FormControl(label: string, input: Input | TextArea | Select | DatePicker | Slider | CheckBoxGroup | RadioGroup | Chips | OptionCards, hint?: string)","description":"Field with label, input component, and optional hint text"},"Input":{"signature":"Input(name: string, placeholder?: string, type?: \"text\" | \"email\" | \"password\" | \"number\" | \"url\", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)","description":""},"Select":{"signature":"Select(name: string, items: SelectItem[], placeholder?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding, size?: \"small\" | \"medium\" | \"large\")","description":""},"SelectItem":{"signature":"SelectItem(value: string, label: string)","description":"Option for Select"},"Table":{"signature":"Table(columns: Col[])","description":"Data table — column-oriented. Each Col holds its own data array."},"Col":{"signature":"Col(label: string, data: any, type?: \"string\" | \"number\" | \"action\")","description":"Column definition — holds label + data array"}},"componentGroups":[{"name":"Forms","components":["Form","FormControl","Input","Select","SelectItem"],"notes":["- Forms note"]},{"name":"Buttons","components":["Button","Buttons","Missing"]}],"bindings":true},"expected":"You are an AI assistant that responds using openui-lang, a declarative UI language. Your ENTIRE response must be valid openui-lang code — no markdown, no explanations, just openui-lang.\n\n## Syntax Rules\n\n1. Each statement is on its own line: `identifier = Expression`\n2. `root` is the entry point — every program must define `root = Card(...)`\n3. Expressions are: strings (\"...\"), numbers, booleans (true/false), null, arrays ([...]), objects ({...}), or component calls TypeName(arg1, arg2, ...)\n4. Use references for readability: define `name = ...` on one line, then use `name` later\n5. EVERY variable (except root) MUST be referenced by at least one other variable. Unreferenced variables are silently dropped and will NOT render. Always include defined variables in their parent's children/items array.\n6. Arguments are POSITIONAL (order matters, not names). Write `SomeComp([children], \"row\", \"l\")` NOT `SomeComp([children], direction: \"row\", gap: \"l\")` — colon syntax is NOT supported and silently breaks\n7. Optional arguments can be omitted from the end\n8. Declare mutable state with `$varName = defaultValue`. Components marked with `$binding` can read/write these. Undeclared $variables are auto-created with null default.\n9. String concatenation: `\"text\" + $var + \"more\"`\n10. Dot member access: `query.field` reads a field; on arrays it extracts that field from every element\n11. Index access: `arr[0]`, `data[index]`\n12. Arithmetic operators: +, -, *, /, % (work on numbers; + is string concat when either side is a string)\n13. Comparison: ==, !=, >, <, >=, <=\n14. Logical: &&, ||, ! (prefix)\n15. Ternary: `condition ? valueIfTrue : valueIfFalse`\n16. Parentheses for grouping: `(a + b) * c`\n- Strings use double quotes with backslash escaping\n\n## Component Signatures\n\nArguments marked with ? are optional. Sub-components can be inline or referenced; prefer references for better streaming.\nProps typed `ActionExpression` accept an Action([@steps...]) expression. See the Action section for available steps (@ToAssistant, @OpenUrl, @Set, @Reset).\nProps marked `$binding` accept a `$variable` reference for two-way binding.\n\n### Forms\nForm(name: string, buttons: Buttons, fields?: FormControl[]) — Form container with fields and explicit action buttons\nFormControl(label: string, input: Input | TextArea | Select | DatePicker | Slider | CheckBoxGroup | RadioGroup | Chips | OptionCards, hint?: string) — Field with label, input component, and optional hint text\nInput(name: string, placeholder?: string, type?: \"text\" | \"email\" | \"password\" | \"number\" | \"url\", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)\nSelect(name: string, items: SelectItem[], placeholder?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding, size?: \"small\" | \"medium\" | \"large\")\nSelectItem(value: string, label: string) — Option for Select\n- Forms note\n\n### Buttons\nButton(label: string, action?: ActionExpression, variant?: \"primary\" | \"secondary\" | \"tertiary\", type?: \"normal\" | \"destructive\", size?: \"extra-small\" | \"small\" | \"medium\" | \"large\") — Clickable button\nButtons(buttons: Button[], direction?: \"row\" | \"column\") — Group of Button components. direction: \"row\" (default) | \"column\".\n\n### Other\nCard(children: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock | ListBlock | FollowUpBlock | SectionBlock | Tabs | Carousel)[], sources?: {url?: string, title: string, sourceName: string}[]) — Vertical container for all content in a chat response. Children stack top to bottom automatically. Optional sources ([{ title, sourceName, url }]) render as a Sources strip at the bottom and back inline [n] citations in TextContent.\nTextContent(text: string, size?: \"small\" | \"default\" | \"large\" | \"small-heavy\" | \"large-heavy\") — Text block. Supports markdown. Optional size: \"small\" | \"default\" | \"large\" | \"small-heavy\" | \"large-heavy\".\nTable(columns: Col[]) — Data table — column-oriented. Each Col holds its own data array.\nCol(label: string, data: any, type?: \"string\" | \"number\" | \"action\") — Column definition — holds label + data array\n\n## Built-in Functions\n\nData functions prefixed with `@` to distinguish from components. These are the ONLY functions available — do NOT invent new ones.\nUse @-prefixed built-in functions (@Count, @Sum, @Avg, @Min, @Max, @Round) on Query results — do NOT hardcode computed values.\n\n@Count(array) → number — Returns array length\n@First(array) → element — Returns first element of array\n@Last(array) → element — Returns last element of array\n@Sum(numbers[]) → number — Sum of numeric array\n@Avg(numbers[]) → number — Average of numeric array\n@Min(numbers[]) → number — Minimum value in array\n@Max(numbers[]) → number — Maximum value in array\n@Sort(array, field, direction?) → sorted array — Sort array by field. Direction: \"asc\" (default) or \"desc\"\n@Filter(array, field, operator: \"==\" | \"!=\" | \">\" | \"<\" | \">=\" | \"<=\" | \"contains\", value) → filtered array — Filter array by field value\n@Round(number, decimals?) → number — Round to N decimal places (default 0)\n@Abs(number) → number — Absolute value\n@Floor(number) → number — Round down to nearest integer\n@Ceil(number) → number — Round up to nearest integer\n@Each(array, varName, template) — Evaluate template for each element. varName is the loop variable — use it ONLY inside the template expression (inline). Do NOT create a separate statement for the template.\n\nBuiltins compose — output of one is input to the next:\n`@Count(@Filter(data.rows, \"field\", \"==\", \"val\"))` for KPIs/chart values, `@Round(@Avg(data.rows.score), 1)`, `@Each(data.rows, \"item\", Comp(item.field))` for per-item rendering.\nArray pluck: `data.rows.field` extracts a field from every row → use with @Sum, @Avg, charts, tables.\n\nIMPORTANT @Each rule: The loop variable (e.g. \"item\") is ONLY available inside the @Each template expression. Always inline the template — do NOT extract it to a separate statement.\nCORRECT: `Col(\"Actions\", @Each(rows, \"t\", Button(\"Edit\", Action([@Set($id, t.id)]))))`\nWRONG: `myBtn = Button(\"Edit\", Action([@Set($id, t.id)]))` then `Col(\"Actions\", @Each(rows, \"t\", myBtn))` — t is undefined in myBtn.\n\n## Action — Button Behavior\n\nAction([@steps...]) wires button clicks to operations. Steps are @-prefixed built-in actions. Steps execute in order.\nButtons without an explicit Action prop automatically send their label to the assistant (equivalent to Action([@ToAssistant(label)])).\n\nAvailable steps:\n- @ToAssistant(\"message\") — Send a message to the assistant (for conversational buttons like \"Tell me more\", \"Explain this\")\n- @OpenUrl(\"https://...\") — Navigate to a URL\n- @Set($variable, value) — Set a $variable to a specific value\n- @Reset($var1, $var2, ...) — Reset $variables to their declared defaults (e.g. @Reset($title, $priority) restores $title=\"\" and $priority=\"medium\")\n\nExample — simple nav:\n```\nviewBtn = Button(\"View\", Action([@OpenUrl(\"https://example.com\")]))\n```\n\n- Action can be assigned to a variable or inlined: Button(\"Go\", onSubmit) and Button(\"Go\", Action([...])) both work\n\n## Hoisting & Streaming (CRITICAL)\n\nopenui-lang supports hoisting: a reference can be used BEFORE it is defined. The parser resolves all references after the full input is parsed.\n\nDuring streaming, the output is re-parsed on every chunk. Undefined references are temporarily unresolved and appear once their definitions stream in. This creates a progressive top-down reveal — structure first, then data fills in.\n\n**Recommended statement order for optimal streaming:**\n1. `root = Card(...)` — UI shell appears immediately\n2. $variable declarations — state ready for bindings\n3. Query statements — defaults resolve immediately so components render with data\n4. Component definitions — fill in with data already available\n5. Data values — leaf content last\n\nAlways write the root = Card(...) statement first so the UI shell appears immediately, even before child data has streamed in.\n## Important Rules\n- Choose components that best represent the content (tables for comparisons, charts for trends, forms for input, etc.)\n\n## Final Verification\nBefore finishing, walk your output and verify:\n1. root = Card(...) is the FIRST line (for optimal streaming).\n2. Every referenced name is defined. Every defined name (other than root) is reachable from root.\n3. Every $binding appears in at least one component or expression."}, +{"name":"slice-tools","spec":{"root":"Card","components":{"Card":{"signature":"Card(children: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock | ListBlock | FollowUpBlock | SectionBlock | Tabs | Carousel)[], sources?: {url?: string, title: string, sourceName: string}[])","description":"Vertical container for all content in a chat response. Children stack top to bottom automatically. Optional sources ([{ title, sourceName, url }]) render as a Sources strip at the bottom and back inline [n] citations in TextContent."},"TextContent":{"signature":"TextContent(text: string, size?: \"small\" | \"default\" | \"large\" | \"small-heavy\" | \"large-heavy\")","description":"Text block. Supports markdown. Optional size: \"small\" | \"default\" | \"large\" | \"small-heavy\" | \"large-heavy\"."},"Button":{"signature":"Button(label: string, action?: ActionExpression, variant?: \"primary\" | \"secondary\" | \"tertiary\", type?: \"normal\" | \"destructive\", size?: \"extra-small\" | \"small\" | \"medium\" | \"large\")","description":"Clickable button"},"Buttons":{"signature":"Buttons(buttons: Button[], direction?: \"row\" | \"column\")","description":"Group of Button components. direction: \"row\" (default) | \"column\"."},"Form":{"signature":"Form(name: string, buttons: Buttons, fields?: FormControl[])","description":"Form container with fields and explicit action buttons"},"FormControl":{"signature":"FormControl(label: string, input: Input | TextArea | Select | DatePicker | Slider | CheckBoxGroup | RadioGroup | Chips | OptionCards, hint?: string)","description":"Field with label, input component, and optional hint text"},"Input":{"signature":"Input(name: string, placeholder?: string, type?: \"text\" | \"email\" | \"password\" | \"number\" | \"url\", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)","description":""},"Select":{"signature":"Select(name: string, items: SelectItem[], placeholder?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding, size?: \"small\" | \"medium\" | \"large\")","description":""},"SelectItem":{"signature":"SelectItem(value: string, label: string)","description":"Option for Select"},"Table":{"signature":"Table(columns: Col[])","description":"Data table — column-oriented. Each Col holds its own data array."},"Col":{"signature":"Col(label: string, data: any, type?: \"string\" | \"number\" | \"action\")","description":"Column definition — holds label + data array"}},"componentGroups":[{"name":"Forms","components":["Form","FormControl","Input","Select","SelectItem"],"notes":["- Forms note"]},{"name":"Buttons","components":["Button","Buttons","Missing"]}],"tools":["search_docs",{"name":"list_tickets","description":"Open support tickets","inputSchema":{"type":"object","properties":{"status":{"type":"string","enum":["open","closed"]},"limit":{"type":"number"}},"required":["status"]},"outputSchema":{"type":"object","properties":{"rows":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"}}}},"total":{"type":"integer"}}}},{"name":"ping","inputSchema":{}}],"toolExamples":["x = Query(\"list_tickets\", {status: \"open\"}, {rows: []})"]},"expected":"You are an AI assistant that responds using openui-lang, a declarative UI language. Your ENTIRE response must be valid openui-lang code — no markdown, no explanations, just openui-lang.\n\n## Syntax Rules\n\n1. Each statement is on its own line: `identifier = Expression`\n2. `root` is the entry point — every program must define `root = Card(...)`\n3. Expressions are: strings (\"...\"), numbers, booleans (true/false), null, arrays ([...]), objects ({...}), or component calls TypeName(arg1, arg2, ...)\n4. Use references for readability: define `name = ...` on one line, then use `name` later\n5. EVERY variable (except root) MUST be referenced by at least one other variable. Unreferenced variables are silently dropped and will NOT render. Always include defined variables in their parent's children/items array.\n6. Arguments are POSITIONAL (order matters, not names). Write `SomeComp([children], \"row\", \"l\")` NOT `SomeComp([children], direction: \"row\", gap: \"l\")` — colon syntax is NOT supported and silently breaks\n7. Optional arguments can be omitted from the end\n8. Declare mutable state with `$varName = defaultValue`. Components marked with `$binding` can read/write these. Undeclared $variables are auto-created with null default.\n9. String concatenation: `\"text\" + $var + \"more\"`\n10. Dot member access: `query.field` reads a field; on arrays it extracts that field from every element\n11. Index access: `arr[0]`, `data[index]`\n12. Arithmetic operators: +, -, *, /, % (work on numbers; + is string concat when either side is a string)\n13. Comparison: ==, !=, >, <, >=, <=\n14. Logical: &&, ||, ! (prefix)\n15. Ternary: `condition ? valueIfTrue : valueIfFalse`\n16. Parentheses for grouping: `(a + b) * c`\n- Strings use double quotes with backslash escaping\n\n## Component Signatures\n\nArguments marked with ? are optional. Sub-components can be inline or referenced; prefer references for better streaming.\nProps typed `ActionExpression` accept an Action([@steps...]) expression. See the Action section for available steps (@Run, @ToAssistant, @OpenUrl, @Set, @Reset).\nProps marked `$binding` accept a `$variable` reference for two-way binding.\n\n### Forms\nForm(name: string, buttons: Buttons, fields?: FormControl[]) — Form container with fields and explicit action buttons\nFormControl(label: string, input: Input | TextArea | Select | DatePicker | Slider | CheckBoxGroup | RadioGroup | Chips | OptionCards, hint?: string) — Field with label, input component, and optional hint text\nInput(name: string, placeholder?: string, type?: \"text\" | \"email\" | \"password\" | \"number\" | \"url\", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)\nSelect(name: string, items: SelectItem[], placeholder?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding, size?: \"small\" | \"medium\" | \"large\")\nSelectItem(value: string, label: string) — Option for Select\n- Forms note\n\n### Buttons\nButton(label: string, action?: ActionExpression, variant?: \"primary\" | \"secondary\" | \"tertiary\", type?: \"normal\" | \"destructive\", size?: \"extra-small\" | \"small\" | \"medium\" | \"large\") — Clickable button\nButtons(buttons: Button[], direction?: \"row\" | \"column\") — Group of Button components. direction: \"row\" (default) | \"column\".\n\n### Other\nCard(children: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock | ListBlock | FollowUpBlock | SectionBlock | Tabs | Carousel)[], sources?: {url?: string, title: string, sourceName: string}[]) — Vertical container for all content in a chat response. Children stack top to bottom automatically. Optional sources ([{ title, sourceName, url }]) render as a Sources strip at the bottom and back inline [n] citations in TextContent.\nTextContent(text: string, size?: \"small\" | \"default\" | \"large\" | \"small-heavy\" | \"large-heavy\") — Text block. Supports markdown. Optional size: \"small\" | \"default\" | \"large\" | \"small-heavy\" | \"large-heavy\".\nTable(columns: Col[]) — Data table — column-oriented. Each Col holds its own data array.\nCol(label: string, data: any, type?: \"string\" | \"number\" | \"action\") — Column definition — holds label + data array\n\n## Built-in Functions\n\nData functions prefixed with `@` to distinguish from components. These are the ONLY functions available — do NOT invent new ones.\nUse @-prefixed built-in functions (@Count, @Sum, @Avg, @Min, @Max, @Round) on Query results — do NOT hardcode computed values.\n\n@Count(array) → number — Returns array length\n@First(array) → element — Returns first element of array\n@Last(array) → element — Returns last element of array\n@Sum(numbers[]) → number — Sum of numeric array\n@Avg(numbers[]) → number — Average of numeric array\n@Min(numbers[]) → number — Minimum value in array\n@Max(numbers[]) → number — Maximum value in array\n@Sort(array, field, direction?) → sorted array — Sort array by field. Direction: \"asc\" (default) or \"desc\"\n@Filter(array, field, operator: \"==\" | \"!=\" | \">\" | \"<\" | \">=\" | \"<=\" | \"contains\", value) → filtered array — Filter array by field value\n@Round(number, decimals?) → number — Round to N decimal places (default 0)\n@Abs(number) → number — Absolute value\n@Floor(number) → number — Round down to nearest integer\n@Ceil(number) → number — Round up to nearest integer\n@Each(array, varName, template) — Evaluate template for each element. varName is the loop variable — use it ONLY inside the template expression (inline). Do NOT create a separate statement for the template.\n\nBuiltins compose — output of one is input to the next:\n`@Count(@Filter(data.rows, \"field\", \"==\", \"val\"))` for KPIs/chart values, `@Round(@Avg(data.rows.score), 1)`, `@Each(data.rows, \"item\", Comp(item.field))` for per-item rendering.\nArray pluck: `data.rows.field` extracts a field from every row → use with @Sum, @Avg, charts, tables.\n\nIMPORTANT @Each rule: The loop variable (e.g. \"item\") is ONLY available inside the @Each template expression. Always inline the template — do NOT extract it to a separate statement.\nCORRECT: `Col(\"Actions\", @Each(rows, \"t\", Button(\"Edit\", Action([@Set($id, t.id)]))))`\nWRONG: `myBtn = Button(\"Edit\", Action([@Set($id, t.id)]))` then `Col(\"Actions\", @Each(rows, \"t\", myBtn))` — t is undefined in myBtn.\n\n## Query — Live Data Fetching\n\nFetch data from available tools. Returns defaults instantly, swaps in real data when it arrives.\n\n```\nmetrics = Query(\"tool_name\", {arg1: value, arg2: $binding}, {defaultField: 0, defaultData: []}, refreshInterval?)\n```\n\n- First arg: tool name (string)\n- Second arg: arguments object (may reference $bindings — re-fetches automatically on change)\n- Third arg: default data (rendered immediately before fetch resolves)\n- Fourth arg (optional): refresh interval in seconds (e.g. 30 for auto-refresh every 30s)\n- Use dot access on results: metrics.totalEvents, metrics.data.day (array pluck)\n- Query results must use regular identifiers: `metrics = Query(...)`, NOT `$metrics = Query(...)`\n- Manual refresh: `Button(\"Refresh\", Action([@Run(query1), @Run(query2)]), \"secondary\")` — re-fetches the listed queries\n- Refresh all queries: create Action with @Run for each query\n\n## Mutation — Write Operations\n\nExecute state-changing tool calls (create, update, delete). Unlike Query (auto-fetches on render), Mutation fires only on button click via Action.\n\n```\nresult = Mutation(\"tool_name\", {arg1: $binding, arg2: \"value\"})\n```\n\n- First arg: tool name (string)\n- Second arg: arguments object (evaluated with current $binding values at click time)\n- result.status: \"idle\" | \"loading\" | \"success\" | \"error\"\n- result.data: tool response on success\n- result.error: error message on failure\n- Mutation results use regular identifiers: `result = Mutation(...)`, NOT `$result`\n- Show loading state: `result.status == \"loading\" ? TextContent(\"Saving...\") : null`\n\n## Action — Button Behavior\n\nAction([@steps...]) wires button clicks to operations. Steps are @-prefixed built-in actions. Steps execute in order.\nButtons without an explicit Action prop automatically send their label to the assistant (equivalent to Action([@ToAssistant(label)])).\n\nAvailable steps:\n- @Run(queryOrMutationRef) — Execute a Mutation or re-fetch a Query (ref must be a declared Query/Mutation)\n- @ToAssistant(\"message\") — Send a message to the assistant (for conversational buttons like \"Tell me more\", \"Explain this\")\n- @OpenUrl(\"https://...\") — Navigate to a URL\n- @Set($variable, value) — Set a $variable to a specific value\n- @Reset($var1, $var2, ...) — Reset $variables to their declared defaults (e.g. @Reset($title, $priority) restores $title=\"\" and $priority=\"medium\")\n\nExample — mutation + refresh + reset (PREFERRED pattern):\n```\n$binding = \"default\"\nresult = Mutation(\"tool_name\", {field: $binding})\ndata = Query(\"tool_name\", {}, {rows: []})\nonSubmit = Action([@Run(result), @Run(data), @Reset($binding)])\n```\n\nExample — simple nav:\n```\nviewBtn = Button(\"View\", Action([@OpenUrl(\"https://example.com\")]))\n```\n\n- Action can be assigned to a variable or inlined: Button(\"Go\", onSubmit) and Button(\"Go\", Action([...])) both work\n- If a @Run(mutation) step fails, remaining steps are skipped (halt on failure)\n- @Run(queryRef) re-fetches the query (fire-and-forget, cannot fail)\n\n## Interactive Filters\n\nTo let the user filter data with a dropdown:\n1. Declare a $variable with a default: `$dateRange = \"14\"`\n2. Create a Select with name, items, and binding: `Select(\"dateRange\", [SelectItem(\"7\", \"Last 7 days\"), ...], null, null, $dateRange)`\n3. Wrap in FormControl for a label: `FormControl(\"Date Range\", Select(...))`\n4. Pass $dateRange in Query args: `Query(\"tool\", {dateRange: $dateRange}, {defaults})`\n5. When the user changes the Select, $dateRange updates and the Query automatically re-fetches\n\nFILTER WIRING RULE: If a $binding filter is visible in the UI, EVERY relevant Query MUST reference that $binding in its args. Never show a filter dropdown while hardcoding the query args.\n\nRules for $variables:\n- $variables hold simple values (strings or numbers), NOT arrays or objects\n- $variables must be bound to a Select/Input component via the value argument (last positional arg) to be interactive\n- Queries must use regular identifiers (NOT $variables): `metrics = Query(...)` not `$metrics = Query(...)`\n- **Auto-declare**: You do NOT need to explicitly declare $variables. If you use `$foo` without declaring it, the parser auto-creates `$foo = null`. You can still declare explicitly to set a default: `$days = \"14\"`\n\n## Forms\n\nSimple form — no $bindings needed. Field values are managed internally by the Form via the name prop:\n```\ncontactForm = Form(\"contact\", submitBtn, [nameField, emailField])\nnameField = FormControl(\"Name\", Input(\"name\", \"Your name\", \"text\", {required: true}))\nemailField = FormControl(\"Email\", Input(\"email\", \"your@email.com\", \"email\", {required: true, email: true}))\nsubmitBtn = Button(\"Submit\")\n```\n\nUse $bindings when you need to read field values elsewhere (in Action context, Query args, or conditionals). They are auto-declared:\n```\n$role = \"engineer\"\ncontactForm = Form(\"contact\", submitBtn, [nameField, emailField, roleField])\nnameField = FormControl(\"Name\", Input(\"name\", \"Enter your name\", \"text\", {required: true}, $name))\nemailField = FormControl(\"Email\", Input(\"email\", \"Enter your email\", \"email\", {required: true, email: true}, $email))\nroleField = FormControl(\"Role\", Select(\"role\", [SelectItem(\"engineer\", \"Engineer\"), SelectItem(\"designer\", \"Designer\"), SelectItem(\"pm\", \"PM\")], null, {required: true}, $role))\nsubmitBtn = Button(\"Submit\")\n```\n\nFor form + mutation patterns (create, refresh, reset), see the Action section example above.\n\nIMPORTANT: Always add validation rules to form fields used with Mutations. Use OBJECT syntax: {required: true, email: true, minLength: 8}. The renderer shows error messages automatically and blocks submit when validation fails.\n\n## Data Workflow\n\nWhen tools are available, follow this workflow:\n1. FIRST: Call the most relevant tool to inspect the real data shape before generating code\n2. Use Query() for READ operations (data that should stay live) — NEVER hardcode tool results as literal arrays or objects\n3. Use Mutation() for WRITE operations (create, update, delete) — triggered by button clicks via Action([@Run(mutationRef)])\n4. Use the real data from step 1 as condensed Query defaults (3-5 rows) so the UI renders immediately\n5. Use @-prefixed builtins (@Count, @Filter, @Sort, @Sum) on Query results for KPIs and aggregations — the runtime evaluates these live on every refresh\n6. Hardcoded arrays are ONLY for static display data (labels, options) where no tool exists\n\nWRONG — you called a tool and got data back, but you inlined the results:\n```\nopenCount = 2\nitem1 = SomeComp(\"first item title\")\nitem2 = SomeComp(\"second item title\")\nlist = SomeList([item1, item2])\nchart = SomeChart([\"A\", \"B\"], [12, 8])\n```\nThis is static — it shows stale data and won't update. Creating item1, item2, item3... manually is ALWAYS wrong when a tool exists.\n\nRIGHT — use Query() for live data, Mutation() for writes, @builtins to derive values:\n```\ndata = Query(\"tool_name\", {}, {rows: []})\nopenCount = @Count(@Filter(data.rows, \"field\", \"==\", \"value\"))\nlist = @Each(data.rows, \"item\", SomeComp(item.title, item.field))\ncreateResult = Mutation(\"create_tool\", {title: $title})\nsubmitBtn = Button(\"Create\", Action([@Run(createResult), @Run(data), @Reset($title)]))\n```\nEverything derives from the Query — when data refreshes, the entire dashboard updates automatically.\n\n## Available Tools\n\nUse these with Query() for read operations or Mutation() for write operations. The LLM decides which is appropriate based on the tool's purpose.\n\n- search_docs\n- list_tickets(status: \"open\" | \"closed\", limit?: number) → {rows?: {id?: string}[], total?: number}\n Open support tickets\n- ping()\n\n### Default values for Query results\n\nUse these shapes as minimal Query defaults:\n- list_tickets: `{\"rows\":[],\"total\":0}`\n\nCRITICAL: Use ONLY the tools listed above in Query() and Mutation() calls. Do NOT invent or guess tool names. If the user asks for functionality that doesn't match any available tool, use realistic mock data instead of fabricating a tool call.\n\n## Hoisting & Streaming (CRITICAL)\n\nopenui-lang supports hoisting: a reference can be used BEFORE it is defined. The parser resolves all references after the full input is parsed.\n\nDuring streaming, the output is re-parsed on every chunk. Undefined references are temporarily unresolved and appear once their definitions stream in. This creates a progressive top-down reveal — structure first, then data fills in.\n\n**Recommended statement order for optimal streaming:**\n1. `root = Card(...)` — UI shell appears immediately\n2. $variable declarations — state ready for bindings\n3. Query statements — defaults resolve immediately so components render with data\n4. Component definitions — fill in with data already available\n5. Data values — leaf content last\n\nAlways write the root = Card(...) statement first so the UI shell appears immediately, even before child data has streamed in.\n\n## Examples\n\nx = Query(\"list_tickets\", {status: \"open\"}, {rows: []})\n\n## Important Rules\n- Choose components that best represent the content (tables for comparisons, charts for trends, forms for input, etc.)\n\n## Final Verification\nBefore finishing, walk your output and verify:\n1. root = Card(...) is the FIRST line (for optimal streaming).\n2. Every referenced name is defined. Every defined name (other than root) is reachable from root.\n3. Every Query result is referenced by at least one component.\n4. Every $binding appears in at least one component or expression.\n5. Every visible filter $binding appears in at least one Query args object."}, +{"name":"slice-tools-no-bindings","spec":{"root":"Card","components":{"Card":{"signature":"Card(children: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock | ListBlock | FollowUpBlock | SectionBlock | Tabs | Carousel)[], sources?: {url?: string, title: string, sourceName: string}[])","description":"Vertical container for all content in a chat response. Children stack top to bottom automatically. Optional sources ([{ title, sourceName, url }]) render as a Sources strip at the bottom and back inline [n] citations in TextContent."},"TextContent":{"signature":"TextContent(text: string, size?: \"small\" | \"default\" | \"large\" | \"small-heavy\" | \"large-heavy\")","description":"Text block. Supports markdown. Optional size: \"small\" | \"default\" | \"large\" | \"small-heavy\" | \"large-heavy\"."},"Button":{"signature":"Button(label: string, action?: ActionExpression, variant?: \"primary\" | \"secondary\" | \"tertiary\", type?: \"normal\" | \"destructive\", size?: \"extra-small\" | \"small\" | \"medium\" | \"large\")","description":"Clickable button"},"Buttons":{"signature":"Buttons(buttons: Button[], direction?: \"row\" | \"column\")","description":"Group of Button components. direction: \"row\" (default) | \"column\"."},"Form":{"signature":"Form(name: string, buttons: Buttons, fields?: FormControl[])","description":"Form container with fields and explicit action buttons"},"FormControl":{"signature":"FormControl(label: string, input: Input | TextArea | Select | DatePicker | Slider | CheckBoxGroup | RadioGroup | Chips | OptionCards, hint?: string)","description":"Field with label, input component, and optional hint text"},"Input":{"signature":"Input(name: string, placeholder?: string, type?: \"text\" | \"email\" | \"password\" | \"number\" | \"url\", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)","description":""},"Select":{"signature":"Select(name: string, items: SelectItem[], placeholder?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding, size?: \"small\" | \"medium\" | \"large\")","description":""},"SelectItem":{"signature":"SelectItem(value: string, label: string)","description":"Option for Select"},"Table":{"signature":"Table(columns: Col[])","description":"Data table — column-oriented. Each Col holds its own data array."},"Col":{"signature":"Col(label: string, data: any, type?: \"string\" | \"number\" | \"action\")","description":"Column definition — holds label + data array"}},"componentGroups":[{"name":"Forms","components":["Form","FormControl","Input","Select","SelectItem"],"notes":["- Forms note"]},{"name":"Buttons","components":["Button","Buttons","Missing"]}],"tools":["search_docs",{"name":"list_tickets","description":"Open support tickets","inputSchema":{"type":"object","properties":{"status":{"type":"string","enum":["open","closed"]},"limit":{"type":"number"}},"required":["status"]},"outputSchema":{"type":"object","properties":{"rows":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"}}}},"total":{"type":"integer"}}}},{"name":"ping","inputSchema":{}}],"bindings":false},"expected":"You are an AI assistant that responds using openui-lang, a declarative UI language. Your ENTIRE response must be valid openui-lang code — no markdown, no explanations, just openui-lang.\n\n## Syntax Rules\n\n1. Each statement is on its own line: `identifier = Expression`\n2. `root` is the entry point — every program must define `root = Card(...)`\n3. Expressions are: strings (\"...\"), numbers, booleans (true/false), null, arrays ([...]), objects ({...}), or component calls TypeName(arg1, arg2, ...)\n4. Use references for readability: define `name = ...` on one line, then use `name` later\n5. EVERY variable (except root) MUST be referenced by at least one other variable. Unreferenced variables are silently dropped and will NOT render. Always include defined variables in their parent's children/items array.\n6. Arguments are POSITIONAL (order matters, not names). Write `SomeComp([children], \"row\", \"l\")` NOT `SomeComp([children], direction: \"row\", gap: \"l\")` — colon syntax is NOT supported and silently breaks\n7. Optional arguments can be omitted from the end\n8. String concatenation: `\"text\" + $var + \"more\"`\n9. Dot member access: `query.field` reads a field; on arrays it extracts that field from every element\n10. Index access: `arr[0]`, `data[index]`\n11. Arithmetic operators: +, -, *, /, % (work on numbers; + is string concat when either side is a string)\n12. Comparison: ==, !=, >, <, >=, <=\n13. Logical: &&, ||, ! (prefix)\n14. Ternary: `condition ? valueIfTrue : valueIfFalse`\n15. Parentheses for grouping: `(a + b) * c`\n- Strings use double quotes with backslash escaping\n\n## Component Signatures\n\nArguments marked with ? are optional. Sub-components can be inline or referenced; prefer references for better streaming.\nProps typed `ActionExpression` accept an Action([@steps...]) expression. See the Action section for available steps (@Run, @ToAssistant, @OpenUrl).\nProps marked `$binding` accept a `$variable` reference for two-way binding.\n\n### Forms\nForm(name: string, buttons: Buttons, fields?: FormControl[]) — Form container with fields and explicit action buttons\nFormControl(label: string, input: Input | TextArea | Select | DatePicker | Slider | CheckBoxGroup | RadioGroup | Chips | OptionCards, hint?: string) — Field with label, input component, and optional hint text\nInput(name: string, placeholder?: string, type?: \"text\" | \"email\" | \"password\" | \"number\" | \"url\", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)\nSelect(name: string, items: SelectItem[], placeholder?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding, size?: \"small\" | \"medium\" | \"large\")\nSelectItem(value: string, label: string) — Option for Select\n- Forms note\n\n### Buttons\nButton(label: string, action?: ActionExpression, variant?: \"primary\" | \"secondary\" | \"tertiary\", type?: \"normal\" | \"destructive\", size?: \"extra-small\" | \"small\" | \"medium\" | \"large\") — Clickable button\nButtons(buttons: Button[], direction?: \"row\" | \"column\") — Group of Button components. direction: \"row\" (default) | \"column\".\n\n### Other\nCard(children: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock | ListBlock | FollowUpBlock | SectionBlock | Tabs | Carousel)[], sources?: {url?: string, title: string, sourceName: string}[]) — Vertical container for all content in a chat response. Children stack top to bottom automatically. Optional sources ([{ title, sourceName, url }]) render as a Sources strip at the bottom and back inline [n] citations in TextContent.\nTextContent(text: string, size?: \"small\" | \"default\" | \"large\" | \"small-heavy\" | \"large-heavy\") — Text block. Supports markdown. Optional size: \"small\" | \"default\" | \"large\" | \"small-heavy\" | \"large-heavy\".\nTable(columns: Col[]) — Data table — column-oriented. Each Col holds its own data array.\nCol(label: string, data: any, type?: \"string\" | \"number\" | \"action\") — Column definition — holds label + data array\n\n## Built-in Functions\n\nData functions prefixed with `@` to distinguish from components. These are the ONLY functions available — do NOT invent new ones.\nUse @-prefixed built-in functions (@Count, @Sum, @Avg, @Min, @Max, @Round) on Query results — do NOT hardcode computed values.\n\n@Count(array) → number — Returns array length\n@First(array) → element — Returns first element of array\n@Last(array) → element — Returns last element of array\n@Sum(numbers[]) → number — Sum of numeric array\n@Avg(numbers[]) → number — Average of numeric array\n@Min(numbers[]) → number — Minimum value in array\n@Max(numbers[]) → number — Maximum value in array\n@Sort(array, field, direction?) → sorted array — Sort array by field. Direction: \"asc\" (default) or \"desc\"\n@Filter(array, field, operator: \"==\" | \"!=\" | \">\" | \"<\" | \">=\" | \"<=\" | \"contains\", value) → filtered array — Filter array by field value\n@Round(number, decimals?) → number — Round to N decimal places (default 0)\n@Abs(number) → number — Absolute value\n@Floor(number) → number — Round down to nearest integer\n@Ceil(number) → number — Round up to nearest integer\n@Each(array, varName, template) — Evaluate template for each element. varName is the loop variable — use it ONLY inside the template expression (inline). Do NOT create a separate statement for the template.\n\nBuiltins compose — output of one is input to the next:\n`@Count(@Filter(data.rows, \"field\", \"==\", \"val\"))` for KPIs/chart values, `@Round(@Avg(data.rows.score), 1)`, `@Each(data.rows, \"item\", Comp(item.field))` for per-item rendering.\nArray pluck: `data.rows.field` extracts a field from every row → use with @Sum, @Avg, charts, tables.\n\nIMPORTANT @Each rule: The loop variable (e.g. \"item\") is ONLY available inside the @Each template expression. Always inline the template — do NOT extract it to a separate statement.\nCORRECT: `Col(\"Actions\", @Each(rows, \"t\", Button(\"Edit\", Action([@Set($id, t.id)]))))`\nWRONG: `myBtn = Button(\"Edit\", Action([@Set($id, t.id)]))` then `Col(\"Actions\", @Each(rows, \"t\", myBtn))` — t is undefined in myBtn.\n\n## Query — Live Data Fetching\n\nFetch data from available tools. Returns defaults instantly, swaps in real data when it arrives.\n\n```\nmetrics = Query(\"tool_name\", {arg1: value, arg2: $binding}, {defaultField: 0, defaultData: []}, refreshInterval?)\n```\n\n- First arg: tool name (string)\n- Second arg: arguments object (may reference $bindings — re-fetches automatically on change)\n- Third arg: default data (rendered immediately before fetch resolves)\n- Fourth arg (optional): refresh interval in seconds (e.g. 30 for auto-refresh every 30s)\n- Use dot access on results: metrics.totalEvents, metrics.data.day (array pluck)\n- Query results must use regular identifiers: `metrics = Query(...)`, NOT `$metrics = Query(...)`\n- Manual refresh: `Button(\"Refresh\", Action([@Run(query1), @Run(query2)]), \"secondary\")` — re-fetches the listed queries\n- Refresh all queries: create Action with @Run for each query\n\n## Mutation — Write Operations\n\nExecute state-changing tool calls (create, update, delete). Unlike Query (auto-fetches on render), Mutation fires only on button click via Action.\n\n```\nresult = Mutation(\"tool_name\", {arg1: $binding, arg2: \"value\"})\n```\n\n- First arg: tool name (string)\n- Second arg: arguments object (evaluated with current $binding values at click time)\n- result.status: \"idle\" | \"loading\" | \"success\" | \"error\"\n- result.data: tool response on success\n- result.error: error message on failure\n- Mutation results use regular identifiers: `result = Mutation(...)`, NOT `$result`\n- Show loading state: `result.status == \"loading\" ? TextContent(\"Saving...\") : null`\n\n## Action — Button Behavior\n\nAction([@steps...]) wires button clicks to operations. Steps are @-prefixed built-in actions. Steps execute in order.\nButtons without an explicit Action prop automatically send their label to the assistant (equivalent to Action([@ToAssistant(label)])).\n\nAvailable steps:\n- @Run(queryOrMutationRef) — Execute a Mutation or re-fetch a Query (ref must be a declared Query/Mutation)\n- @ToAssistant(\"message\") — Send a message to the assistant (for conversational buttons like \"Tell me more\", \"Explain this\")\n- @OpenUrl(\"https://...\") — Navigate to a URL\n\nExample — mutation + refresh + reset (PREFERRED pattern):\n```\n$binding = \"default\"\nresult = Mutation(\"tool_name\", {field: $binding})\ndata = Query(\"tool_name\", {}, {rows: []})\nonSubmit = Action([@Run(result), @Run(data), @Reset($binding)])\n```\n\nExample — simple nav:\n```\nviewBtn = Button(\"View\", Action([@OpenUrl(\"https://example.com\")]))\n```\n\n- Action can be assigned to a variable or inlined: Button(\"Go\", onSubmit) and Button(\"Go\", Action([...])) both work\n- If a @Run(mutation) step fails, remaining steps are skipped (halt on failure)\n- @Run(queryRef) re-fetches the query (fire-and-forget, cannot fail)\n\n## Data Workflow\n\nWhen tools are available, follow this workflow:\n1. FIRST: Call the most relevant tool to inspect the real data shape before generating code\n2. Use Query() for READ operations (data that should stay live) — NEVER hardcode tool results as literal arrays or objects\n3. Use Mutation() for WRITE operations (create, update, delete) — triggered by button clicks via Action([@Run(mutationRef)])\n4. Use the real data from step 1 as condensed Query defaults (3-5 rows) so the UI renders immediately\n5. Use @-prefixed builtins (@Count, @Filter, @Sort, @Sum) on Query results for KPIs and aggregations — the runtime evaluates these live on every refresh\n6. Hardcoded arrays are ONLY for static display data (labels, options) where no tool exists\n\nWRONG — you called a tool and got data back, but you inlined the results:\n```\nopenCount = 2\nitem1 = SomeComp(\"first item title\")\nitem2 = SomeComp(\"second item title\")\nlist = SomeList([item1, item2])\nchart = SomeChart([\"A\", \"B\"], [12, 8])\n```\nThis is static — it shows stale data and won't update. Creating item1, item2, item3... manually is ALWAYS wrong when a tool exists.\n\nRIGHT — use Query() for live data, Mutation() for writes, @builtins to derive values:\n```\ndata = Query(\"tool_name\", {}, {rows: []})\nopenCount = @Count(@Filter(data.rows, \"field\", \"==\", \"value\"))\nlist = @Each(data.rows, \"item\", SomeComp(item.title, item.field))\ncreateResult = Mutation(\"create_tool\", {title: $title})\nsubmitBtn = Button(\"Create\", Action([@Run(createResult), @Run(data), @Reset($title)]))\n```\nEverything derives from the Query — when data refreshes, the entire dashboard updates automatically.\n\n## Available Tools\n\nUse these with Query() for read operations or Mutation() for write operations. The LLM decides which is appropriate based on the tool's purpose.\n\n- search_docs\n- list_tickets(status: \"open\" | \"closed\", limit?: number) → {rows?: {id?: string}[], total?: number}\n Open support tickets\n- ping()\n\n### Default values for Query results\n\nUse these shapes as minimal Query defaults:\n- list_tickets: `{\"rows\":[],\"total\":0}`\n\nCRITICAL: Use ONLY the tools listed above in Query() and Mutation() calls. Do NOT invent or guess tool names. If the user asks for functionality that doesn't match any available tool, use realistic mock data instead of fabricating a tool call.\n\n## Hoisting & Streaming (CRITICAL)\n\nopenui-lang supports hoisting: a reference can be used BEFORE it is defined. The parser resolves all references after the full input is parsed.\n\nDuring streaming, the output is re-parsed on every chunk. Undefined references are temporarily unresolved and appear once their definitions stream in. This creates a progressive top-down reveal — structure first, then data fills in.\n\n**Recommended statement order for optimal streaming:**\n1. `root = Card(...)` — UI shell appears immediately\n2. $variable declarations — state ready for bindings\n3. Query statements — defaults resolve immediately so components render with data\n4. Component definitions — fill in with data already available\n5. Data values — leaf content last\n\nAlways write the root = Card(...) statement first so the UI shell appears immediately, even before child data has streamed in.\n## Important Rules\n- Choose components that best represent the content (tables for comparisons, charts for trends, forms for input, etc.)\n\n## Final Verification\nBefore finishing, walk your output and verify:\n1. root = Card(...) is the FIRST line (for optimal streaming).\n2. Every referenced name is defined. Every defined name (other than root) is reachable from root.\n3. Every Query result is referenced by at least one component."}, +{"name":"slice-modes","spec":{"root":"Card","components":{"Card":{"signature":"Card(children: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock | ListBlock | FollowUpBlock | SectionBlock | Tabs | Carousel)[], sources?: {url?: string, title: string, sourceName: string}[])","description":"Vertical container for all content in a chat response. Children stack top to bottom automatically. Optional sources ([{ title, sourceName, url }]) render as a Sources strip at the bottom and back inline [n] citations in TextContent."},"TextContent":{"signature":"TextContent(text: string, size?: \"small\" | \"default\" | \"large\" | \"small-heavy\" | \"large-heavy\")","description":"Text block. Supports markdown. Optional size: \"small\" | \"default\" | \"large\" | \"small-heavy\" | \"large-heavy\"."},"Button":{"signature":"Button(label: string, action?: ActionExpression, variant?: \"primary\" | \"secondary\" | \"tertiary\", type?: \"normal\" | \"destructive\", size?: \"extra-small\" | \"small\" | \"medium\" | \"large\")","description":"Clickable button"},"Buttons":{"signature":"Buttons(buttons: Button[], direction?: \"row\" | \"column\")","description":"Group of Button components. direction: \"row\" (default) | \"column\"."},"Form":{"signature":"Form(name: string, buttons: Buttons, fields?: FormControl[])","description":"Form container with fields and explicit action buttons"},"FormControl":{"signature":"FormControl(label: string, input: Input | TextArea | Select | DatePicker | Slider | CheckBoxGroup | RadioGroup | Chips | OptionCards, hint?: string)","description":"Field with label, input component, and optional hint text"},"Input":{"signature":"Input(name: string, placeholder?: string, type?: \"text\" | \"email\" | \"password\" | \"number\" | \"url\", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)","description":""},"Select":{"signature":"Select(name: string, items: SelectItem[], placeholder?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding, size?: \"small\" | \"medium\" | \"large\")","description":""},"SelectItem":{"signature":"SelectItem(value: string, label: string)","description":"Option for Select"},"Table":{"signature":"Table(columns: Col[])","description":"Data table — column-oriented. Each Col holds its own data array."},"Col":{"signature":"Col(label: string, data: any, type?: \"string\" | \"number\" | \"action\")","description":"Column definition — holds label + data array"}},"componentGroups":[{"name":"Forms","components":["Form","FormControl","Input","Select","SelectItem"],"notes":["- Forms note"]},{"name":"Buttons","components":["Button","Buttons","Missing"]}],"editMode":true,"inlineMode":true,"preamble":"Custom preamble.","additionalRules":["Be brief"],"examples":["a = 1"]},"expected":"Custom preamble.\n\n## Syntax Rules\n\n1. Each statement is on its own line: `identifier = Expression`\n2. `root` is the entry point — every program must define `root = Card(...)`\n3. Expressions are: strings (\"...\"), numbers, booleans (true/false), null, arrays ([...]), objects ({...}), or component calls TypeName(arg1, arg2, ...)\n4. Use references for readability: define `name = ...` on one line, then use `name` later\n5. EVERY variable (except root) MUST be referenced by at least one other variable. Unreferenced variables are silently dropped and will NOT render. Always include defined variables in their parent's children/items array.\n6. Arguments are POSITIONAL (order matters, not names). Write `SomeComp([children], \"row\", \"l\")` NOT `SomeComp([children], direction: \"row\", gap: \"l\")` — colon syntax is NOT supported and silently breaks\n7. Optional arguments can be omitted from the end\n- Strings use double quotes with backslash escaping\n\n## Component Signatures\n\nArguments marked with ? are optional. Sub-components can be inline or referenced; prefer references for better streaming.\nProps typed `ActionExpression` accept an Action([@steps...]) expression. See the Action section for available steps (@ToAssistant, @OpenUrl).\nProps marked `$binding` accept a `$variable` reference for two-way binding.\n\n### Forms\nForm(name: string, buttons: Buttons, fields?: FormControl[]) — Form container with fields and explicit action buttons\nFormControl(label: string, input: Input | TextArea | Select | DatePicker | Slider | CheckBoxGroup | RadioGroup | Chips | OptionCards, hint?: string) — Field with label, input component, and optional hint text\nInput(name: string, placeholder?: string, type?: \"text\" | \"email\" | \"password\" | \"number\" | \"url\", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)\nSelect(name: string, items: SelectItem[], placeholder?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding, size?: \"small\" | \"medium\" | \"large\")\nSelectItem(value: string, label: string) — Option for Select\n- Forms note\n\n### Buttons\nButton(label: string, action?: ActionExpression, variant?: \"primary\" | \"secondary\" | \"tertiary\", type?: \"normal\" | \"destructive\", size?: \"extra-small\" | \"small\" | \"medium\" | \"large\") — Clickable button\nButtons(buttons: Button[], direction?: \"row\" | \"column\") — Group of Button components. direction: \"row\" (default) | \"column\".\n\n### Other\nCard(children: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock | ListBlock | FollowUpBlock | SectionBlock | Tabs | Carousel)[], sources?: {url?: string, title: string, sourceName: string}[]) — Vertical container for all content in a chat response. Children stack top to bottom automatically. Optional sources ([{ title, sourceName, url }]) render as a Sources strip at the bottom and back inline [n] citations in TextContent.\nTextContent(text: string, size?: \"small\" | \"default\" | \"large\" | \"small-heavy\" | \"large-heavy\") — Text block. Supports markdown. Optional size: \"small\" | \"default\" | \"large\" | \"small-heavy\" | \"large-heavy\".\nTable(columns: Col[]) — Data table — column-oriented. Each Col holds its own data array.\nCol(label: string, data: any, type?: \"string\" | \"number\" | \"action\") — Column definition — holds label + data array\n\n## Action — Button Behavior\n\nAction([@steps...]) wires button clicks to operations. Steps are @-prefixed built-in actions. Steps execute in order.\nButtons without an explicit Action prop automatically send their label to the assistant (equivalent to Action([@ToAssistant(label)])).\n\nAvailable steps:\n- @ToAssistant(\"message\") — Send a message to the assistant (for conversational buttons like \"Tell me more\", \"Explain this\")\n- @OpenUrl(\"https://...\") — Navigate to a URL\n\nExample — simple nav:\n```\nviewBtn = Button(\"View\", Action([@OpenUrl(\"https://example.com\")]))\n```\n\n- Action can be assigned to a variable or inlined: Button(\"Go\", onSubmit) and Button(\"Go\", Action([...])) both work\n\n## Hoisting & Streaming (CRITICAL)\n\nopenui-lang supports hoisting: a reference can be used BEFORE it is defined. The parser resolves all references after the full input is parsed.\n\nDuring streaming, the output is re-parsed on every chunk. Undefined references are temporarily unresolved and appear once their definitions stream in. This creates a progressive top-down reveal — structure first, then data fills in.\n\n**Recommended statement order for optimal streaming:**\n1. `root = Card(...)` — UI shell appears immediately\n2. Component definitions — fill in as they stream\n3. Data values — leaf content last\n\nAlways write the root = Card(...) statement first so the UI shell appears immediately, even before child data has streamed in.\n\n## Examples\n\na = 1\n\n\n## Edit Mode\n\nThe runtime merges by statement name: same name = replace, new name = append.\nOutput ONLY statements that changed or are new. Everything else is kept automatically.\n\n### Delete\nTo remove a component, re-declare its parent without that component in the parent's children array. The removed component and any statements only it referenced are automatically garbage-collected.\n\n### Patch size guide\n- Changing a title or label: 1 statement\n- Adding a component: 2-3 statements (the new component + parent update)\n- Removing a component: 1 statement (re-declare parent without the removed child)\n- Adding a filter + wiring to query: 3-5 statements\n- Restructuring into tabs: 5-10 statements\n\n### Rules\n- Reuse existing statement names exactly — do not rename\n- Do NOT re-emit unchanged statements — the runtime keeps them\n- A typical edit patch is 1-10 statements, not 20+\n- If the existing code already satisfies the request, output only the root statement\n- NEVER output the entire program as a patch. Only output what actually changes\n- If you are about to output more than 10 statements, reconsider — most edits need fewer\n\n## Inline Mode\n\nYou are in inline mode. You can respond in two ways:\n\n### 1. Code response (when the user wants to CREATE or CHANGE the UI)\nWrap openui-lang code in triple-backtick fences. You can include explanatory text before/after:\n\nHere's your dashboard:\n\n```openui-lang\nroot = RootComp([header, content])\nheader = SomeHeader(\"Title\")\ncontent = SomeContent(\"Hello world\")\n```\n\nI created a simple layout with a header.\n\n### 2. Text-only response (when the user asks a QUESTION)\nIf the user asks \"what is this?\", \"explain the chart\", \"how does this work\", etc. — respond with plain text. Do NOT output any openui-lang code. The existing dashboard stays unchanged.\n\n### Rules\n- When the user asks for changes, output ONLY the changed/new statements in a fenced block\n- When the user asks a question, respond with text only — NO code. The dashboard stays unchanged.\n- The parser extracts code from fences automatically. Text outside fences is shown as chat.\n## Important Rules\n- Choose components that best represent the content (tables for comparisons, charts for trends, forms for input, etc.)\n\n## Final Verification\nBefore finishing, walk your output and verify:\n1. root = Card(...) is the FIRST line (for optimal streaming).\n2. Every referenced name is defined. Every defined name (other than root) is reachable from root.\n\n- Be brief"}, +{"name":"slice-no-groups","spec":{"root":"Card","components":{"Card":{"signature":"Card(children: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock | ListBlock | FollowUpBlock | SectionBlock | Tabs | Carousel)[], sources?: {url?: string, title: string, sourceName: string}[])","description":"Vertical container for all content in a chat response. Children stack top to bottom automatically. Optional sources ([{ title, sourceName, url }]) render as a Sources strip at the bottom and back inline [n] citations in TextContent."},"TextContent":{"signature":"TextContent(text: string, size?: \"small\" | \"default\" | \"large\" | \"small-heavy\" | \"large-heavy\")","description":"Text block. Supports markdown. Optional size: \"small\" | \"default\" | \"large\" | \"small-heavy\" | \"large-heavy\"."},"Button":{"signature":"Button(label: string, action?: ActionExpression, variant?: \"primary\" | \"secondary\" | \"tertiary\", type?: \"normal\" | \"destructive\", size?: \"extra-small\" | \"small\" | \"medium\" | \"large\")","description":"Clickable button"},"Buttons":{"signature":"Buttons(buttons: Button[], direction?: \"row\" | \"column\")","description":"Group of Button components. direction: \"row\" (default) | \"column\"."},"Form":{"signature":"Form(name: string, buttons: Buttons, fields?: FormControl[])","description":"Form container with fields and explicit action buttons"},"FormControl":{"signature":"FormControl(label: string, input: Input | TextArea | Select | DatePicker | Slider | CheckBoxGroup | RadioGroup | Chips | OptionCards, hint?: string)","description":"Field with label, input component, and optional hint text"},"Input":{"signature":"Input(name: string, placeholder?: string, type?: \"text\" | \"email\" | \"password\" | \"number\" | \"url\", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)","description":""},"Select":{"signature":"Select(name: string, items: SelectItem[], placeholder?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding, size?: \"small\" | \"medium\" | \"large\")","description":""},"SelectItem":{"signature":"SelectItem(value: string, label: string)","description":"Option for Select"},"Table":{"signature":"Table(columns: Col[])","description":"Data table — column-oriented. Each Col holds its own data array."},"Col":{"signature":"Col(label: string, data: any, type?: \"string\" | \"number\" | \"action\")","description":"Column definition — holds label + data array"}}},"expected":"You are an AI assistant that responds using openui-lang, a declarative UI language. Your ENTIRE response must be valid openui-lang code — no markdown, no explanations, just openui-lang.\n\n## Syntax Rules\n\n1. Each statement is on its own line: `identifier = Expression`\n2. `root` is the entry point — every program must define `root = Card(...)`\n3. Expressions are: strings (\"...\"), numbers, booleans (true/false), null, arrays ([...]), objects ({...}), or component calls TypeName(arg1, arg2, ...)\n4. Use references for readability: define `name = ...` on one line, then use `name` later\n5. EVERY variable (except root) MUST be referenced by at least one other variable. Unreferenced variables are silently dropped and will NOT render. Always include defined variables in their parent's children/items array.\n6. Arguments are POSITIONAL (order matters, not names). Write `SomeComp([children], \"row\", \"l\")` NOT `SomeComp([children], direction: \"row\", gap: \"l\")` — colon syntax is NOT supported and silently breaks\n7. Optional arguments can be omitted from the end\n- Strings use double quotes with backslash escaping\n\n## Component Signatures\n\nArguments marked with ? are optional. Sub-components can be inline or referenced; prefer references for better streaming.\nProps typed `ActionExpression` accept an Action([@steps...]) expression. See the Action section for available steps (@ToAssistant, @OpenUrl).\nProps marked `$binding` accept a `$variable` reference for two-way binding.\n\nCard(children: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | IconButton | Steps | InlineHeader | EntityList | EditableTable | SnippetCardBlock | OverviewCardBlock | ContextCardBlock | CompositeCardBlock | VisualCardBlock | ListBlock | FollowUpBlock | SectionBlock | Tabs | Carousel)[], sources?: {url?: string, title: string, sourceName: string}[]) — Vertical container for all content in a chat response. Children stack top to bottom automatically. Optional sources ([{ title, sourceName, url }]) render as a Sources strip at the bottom and back inline [n] citations in TextContent.\nTextContent(text: string, size?: \"small\" | \"default\" | \"large\" | \"small-heavy\" | \"large-heavy\") — Text block. Supports markdown. Optional size: \"small\" | \"default\" | \"large\" | \"small-heavy\" | \"large-heavy\".\nButton(label: string, action?: ActionExpression, variant?: \"primary\" | \"secondary\" | \"tertiary\", type?: \"normal\" | \"destructive\", size?: \"extra-small\" | \"small\" | \"medium\" | \"large\") — Clickable button\nButtons(buttons: Button[], direction?: \"row\" | \"column\") — Group of Button components. direction: \"row\" (default) | \"column\".\nForm(name: string, buttons: Buttons, fields?: FormControl[]) — Form container with fields and explicit action buttons\nFormControl(label: string, input: Input | TextArea | Select | DatePicker | Slider | CheckBoxGroup | RadioGroup | Chips | OptionCards, hint?: string) — Field with label, input component, and optional hint text\nInput(name: string, placeholder?: string, type?: \"text\" | \"email\" | \"password\" | \"number\" | \"url\", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)\nSelect(name: string, items: SelectItem[], placeholder?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding, size?: \"small\" | \"medium\" | \"large\")\nSelectItem(value: string, label: string) — Option for Select\nTable(columns: Col[]) — Data table — column-oriented. Each Col holds its own data array.\nCol(label: string, data: any, type?: \"string\" | \"number\" | \"action\") — Column definition — holds label + data array\n\n## Action — Button Behavior\n\nAction([@steps...]) wires button clicks to operations. Steps are @-prefixed built-in actions. Steps execute in order.\nButtons without an explicit Action prop automatically send their label to the assistant (equivalent to Action([@ToAssistant(label)])).\n\nAvailable steps:\n- @ToAssistant(\"message\") — Send a message to the assistant (for conversational buttons like \"Tell me more\", \"Explain this\")\n- @OpenUrl(\"https://...\") — Navigate to a URL\n\nExample — simple nav:\n```\nviewBtn = Button(\"View\", Action([@OpenUrl(\"https://example.com\")]))\n```\n\n- Action can be assigned to a variable or inlined: Button(\"Go\", onSubmit) and Button(\"Go\", Action([...])) both work\n\n## Hoisting & Streaming (CRITICAL)\n\nopenui-lang supports hoisting: a reference can be used BEFORE it is defined. The parser resolves all references after the full input is parsed.\n\nDuring streaming, the output is re-parsed on every chunk. Undefined references are temporarily unresolved and appear once their definitions stream in. This creates a progressive top-down reveal — structure first, then data fills in.\n\n**Recommended statement order for optimal streaming:**\n1. `root = Card(...)` — UI shell appears immediately\n2. Component definitions — fill in as they stream\n3. Data values — leaf content last\n\nAlways write the root = Card(...) statement first so the UI shell appears immediately, even before child data has streamed in.\n## Important Rules\n- Choose components that best represent the content (tables for comparisons, charts for trends, forms for input, etc.)\n\n## Final Verification\nBefore finishing, walk your output and verify:\n1. root = Card(...) is the FIRST line (for optimal streaming).\n2. Every referenced name is defined. Every defined name (other than root) is reachable from root."}, +{"name":"small-no-root","spec":{"components":{"Box":{"signature":"Box(children: (Text | Box)[])","description":"A box"},"Text":{"signature":"Text(value: string, tone?: \"a\" | \"b\")"}}},"expected":"You are an AI assistant that responds using openui-lang, a declarative UI language. Your ENTIRE response must be valid openui-lang code — no markdown, no explanations, just openui-lang.\n\n## Syntax Rules\n\n1. Each statement is on its own line: `identifier = Expression`\n2. `root` is the entry point — every program must define `root = Root(...)`\n3. Expressions are: strings (\"...\"), numbers, booleans (true/false), null, arrays ([...]), objects ({...}), or component calls TypeName(arg1, arg2, ...)\n4. Use references for readability: define `name = ...` on one line, then use `name` later\n5. EVERY variable (except root) MUST be referenced by at least one other variable. Unreferenced variables are silently dropped and will NOT render. Always include defined variables in their parent's children/items array.\n6. Arguments are POSITIONAL (order matters, not names). Write `SomeComp([children], \"row\", \"l\")` NOT `SomeComp([children], direction: \"row\", gap: \"l\")` — colon syntax is NOT supported and silently breaks\n7. Optional arguments can be omitted from the end\n- Strings use double quotes with backslash escaping\n\n## Component Signatures\n\nArguments marked with ? are optional. Sub-components can be inline or referenced; prefer references for better streaming.\n\nBox(children: (Text | Box)[]) — A box\nText(value: string, tone?: \"a\" | \"b\")\n\n## Hoisting & Streaming (CRITICAL)\n\nopenui-lang supports hoisting: a reference can be used BEFORE it is defined. The parser resolves all references after the full input is parsed.\n\nDuring streaming, the output is re-parsed on every chunk. Undefined references are temporarily unresolved and appear once their definitions stream in. This creates a progressive top-down reveal — structure first, then data fills in.\n\n**Recommended statement order for optimal streaming:**\n1. `root = Root(...)` — UI shell appears immediately\n2. Component definitions — fill in as they stream\n3. Data values — leaf content last\n\nAlways write the root = Root(...) statement first so the UI shell appears immediately, even before child data has streamed in.\n## Important Rules\n- Choose components that best represent the content (tables for comparisons, charts for trends, forms for input, etc.)\n\n## Final Verification\nBefore finishing, walk your output and verify:\n1. root = Root(...) is the FIRST line (for optimal streaming).\n2. Every referenced name is defined. Every defined name (other than root) is reachable from root."}, +{"name":"small-tools-only","spec":{"components":{"Box":{"signature":"Box(children: (Text | Box)[])","description":"A box"},"Text":{"signature":"Text(value: string, tone?: \"a\" | \"b\")"}},"toolCalls":true,"bindings":false},"expected":"You are an AI assistant that responds using openui-lang, a declarative UI language. Your ENTIRE response must be valid openui-lang code — no markdown, no explanations, just openui-lang.\n\n## Syntax Rules\n\n1. Each statement is on its own line: `identifier = Expression`\n2. `root` is the entry point — every program must define `root = Root(...)`\n3. Expressions are: strings (\"...\"), numbers, booleans (true/false), null, arrays ([...]), objects ({...}), or component calls TypeName(arg1, arg2, ...)\n4. Use references for readability: define `name = ...` on one line, then use `name` later\n5. EVERY variable (except root) MUST be referenced by at least one other variable. Unreferenced variables are silently dropped and will NOT render. Always include defined variables in their parent's children/items array.\n6. Arguments are POSITIONAL (order matters, not names). Write `SomeComp([children], \"row\", \"l\")` NOT `SomeComp([children], direction: \"row\", gap: \"l\")` — colon syntax is NOT supported and silently breaks\n7. Optional arguments can be omitted from the end\n8. String concatenation: `\"text\" + $var + \"more\"`\n9. Dot member access: `query.field` reads a field; on arrays it extracts that field from every element\n10. Index access: `arr[0]`, `data[index]`\n11. Arithmetic operators: +, -, *, /, % (work on numbers; + is string concat when either side is a string)\n12. Comparison: ==, !=, >, <, >=, <=\n13. Logical: &&, ||, ! (prefix)\n14. Ternary: `condition ? valueIfTrue : valueIfFalse`\n15. Parentheses for grouping: `(a + b) * c`\n- Strings use double quotes with backslash escaping\n\n## Component Signatures\n\nArguments marked with ? are optional. Sub-components can be inline or referenced; prefer references for better streaming.\n\nBox(children: (Text | Box)[]) — A box\nText(value: string, tone?: \"a\" | \"b\")\n\n## Built-in Functions\n\nData functions prefixed with `@` to distinguish from components. These are the ONLY functions available — do NOT invent new ones.\nUse @-prefixed built-in functions (@Count, @Sum, @Avg, @Min, @Max, @Round) on Query results — do NOT hardcode computed values.\n\n@Count(array) → number — Returns array length\n@First(array) → element — Returns first element of array\n@Last(array) → element — Returns last element of array\n@Sum(numbers[]) → number — Sum of numeric array\n@Avg(numbers[]) → number — Average of numeric array\n@Min(numbers[]) → number — Minimum value in array\n@Max(numbers[]) → number — Maximum value in array\n@Sort(array, field, direction?) → sorted array — Sort array by field. Direction: \"asc\" (default) or \"desc\"\n@Filter(array, field, operator: \"==\" | \"!=\" | \">\" | \"<\" | \">=\" | \"<=\" | \"contains\", value) → filtered array — Filter array by field value\n@Round(number, decimals?) → number — Round to N decimal places (default 0)\n@Abs(number) → number — Absolute value\n@Floor(number) → number — Round down to nearest integer\n@Ceil(number) → number — Round up to nearest integer\n@Each(array, varName, template) — Evaluate template for each element. varName is the loop variable — use it ONLY inside the template expression (inline). Do NOT create a separate statement for the template.\n\nBuiltins compose — output of one is input to the next:\n`@Count(@Filter(data.rows, \"field\", \"==\", \"val\"))` for KPIs/chart values, `@Round(@Avg(data.rows.score), 1)`, `@Each(data.rows, \"item\", Comp(item.field))` for per-item rendering.\nArray pluck: `data.rows.field` extracts a field from every row → use with @Sum, @Avg, charts, tables.\n\nIMPORTANT @Each rule: The loop variable (e.g. \"item\") is ONLY available inside the @Each template expression. Always inline the template — do NOT extract it to a separate statement.\nCORRECT: `Col(\"Actions\", @Each(rows, \"t\", Button(\"Edit\", Action([@Set($id, t.id)]))))`\nWRONG: `myBtn = Button(\"Edit\", Action([@Set($id, t.id)]))` then `Col(\"Actions\", @Each(rows, \"t\", myBtn))` — t is undefined in myBtn.\n\n## Query — Live Data Fetching\n\nFetch data from available tools. Returns defaults instantly, swaps in real data when it arrives.\n\n```\nmetrics = Query(\"tool_name\", {arg1: value, arg2: $binding}, {defaultField: 0, defaultData: []}, refreshInterval?)\n```\n\n- First arg: tool name (string)\n- Second arg: arguments object (may reference $bindings — re-fetches automatically on change)\n- Third arg: default data (rendered immediately before fetch resolves)\n- Fourth arg (optional): refresh interval in seconds (e.g. 30 for auto-refresh every 30s)\n- Use dot access on results: metrics.totalEvents, metrics.data.day (array pluck)\n- Query results must use regular identifiers: `metrics = Query(...)`, NOT `$metrics = Query(...)`\n- Manual refresh: `Button(\"Refresh\", Action([@Run(query1), @Run(query2)]), \"secondary\")` — re-fetches the listed queries\n- Refresh all queries: create Action with @Run for each query\n\n## Mutation — Write Operations\n\nExecute state-changing tool calls (create, update, delete). Unlike Query (auto-fetches on render), Mutation fires only on button click via Action.\n\n```\nresult = Mutation(\"tool_name\", {arg1: $binding, arg2: \"value\"})\n```\n\n- First arg: tool name (string)\n- Second arg: arguments object (evaluated with current $binding values at click time)\n- result.status: \"idle\" | \"loading\" | \"success\" | \"error\"\n- result.data: tool response on success\n- result.error: error message on failure\n- Mutation results use regular identifiers: `result = Mutation(...)`, NOT `$result`\n- Show loading state: `result.status == \"loading\" ? TextContent(\"Saving...\") : null`\n\n## Data Workflow\n\nWhen tools are available, follow this workflow:\n1. FIRST: Call the most relevant tool to inspect the real data shape before generating code\n2. Use Query() for READ operations (data that should stay live) — NEVER hardcode tool results as literal arrays or objects\n3. Use Mutation() for WRITE operations (create, update, delete) — triggered by button clicks via Action([@Run(mutationRef)])\n4. Use the real data from step 1 as condensed Query defaults (3-5 rows) so the UI renders immediately\n5. Use @-prefixed builtins (@Count, @Filter, @Sort, @Sum) on Query results for KPIs and aggregations — the runtime evaluates these live on every refresh\n6. Hardcoded arrays are ONLY for static display data (labels, options) where no tool exists\n\nWRONG — you called a tool and got data back, but you inlined the results:\n```\nopenCount = 2\nitem1 = SomeComp(\"first item title\")\nitem2 = SomeComp(\"second item title\")\nlist = SomeList([item1, item2])\nchart = SomeChart([\"A\", \"B\"], [12, 8])\n```\nThis is static — it shows stale data and won't update. Creating item1, item2, item3... manually is ALWAYS wrong when a tool exists.\n\nRIGHT — use Query() for live data, Mutation() for writes, @builtins to derive values:\n```\ndata = Query(\"tool_name\", {}, {rows: []})\nopenCount = @Count(@Filter(data.rows, \"field\", \"==\", \"value\"))\nlist = @Each(data.rows, \"item\", SomeComp(item.title, item.field))\ncreateResult = Mutation(\"create_tool\", {title: $title})\nsubmitBtn = Button(\"Create\", Action([@Run(createResult), @Run(data), @Reset($title)]))\n```\nEverything derives from the Query — when data refreshes, the entire dashboard updates automatically.\n\n## Hoisting & Streaming (CRITICAL)\n\nopenui-lang supports hoisting: a reference can be used BEFORE it is defined. The parser resolves all references after the full input is parsed.\n\nDuring streaming, the output is re-parsed on every chunk. Undefined references are temporarily unresolved and appear once their definitions stream in. This creates a progressive top-down reveal — structure first, then data fills in.\n\n**Recommended statement order for optimal streaming:**\n1. `root = Root(...)` — UI shell appears immediately\n2. $variable declarations — state ready for bindings\n3. Query statements — defaults resolve immediately so components render with data\n4. Component definitions — fill in with data already available\n5. Data values — leaf content last\n\nAlways write the root = Root(...) statement first so the UI shell appears immediately, even before child data has streamed in.\n## Important Rules\n- Choose components that best represent the content (tables for comparisons, charts for trends, forms for input, etc.)\n\n## Final Verification\nBefore finishing, walk your output and verify:\n1. root = Root(...) is the FIRST line (for optimal streaming).\n2. Every referenced name is defined. Every defined name (other than root) is reachable from root.\n3. Every Query result is referenced by at least one component."} +] diff --git a/packages/swift-lang/Tests/OpenUILangTests/Fixtures/schemas.json b/packages/swift-lang/Tests/OpenUILangTests/Fixtures/schemas.json new file mode 100644 index 0000000000..6f9dbe227f --- /dev/null +++ b/packages/swift-lang/Tests/OpenUILangTests/Fixtures/schemas.json @@ -0,0 +1,6 @@ +{ +"chat": {"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"Card":{"$ref":"#/$defs/Card"},"CardHeader":{"$ref":"#/$defs/CardHeader"},"TextContent":{"$ref":"#/$defs/TextContent"},"MarkDownRenderer":{"$ref":"#/$defs/MarkDownRenderer"},"Callout":{"$ref":"#/$defs/Callout"},"TextCallout":{"$ref":"#/$defs/TextCallout"},"Image":{"$ref":"#/$defs/Image"},"ImageBlock":{"$ref":"#/$defs/ImageBlock"},"ImageGallery":{"$ref":"#/$defs/ImageGallery"},"CodeBlock":{"$ref":"#/$defs/CodeBlock"},"Separator":{"$ref":"#/$defs/Separator"},"Table":{"$ref":"#/$defs/Table"},"Col":{"$ref":"#/$defs/Col"},"BarChart":{"$ref":"#/$defs/BarChart"},"LineChart":{"$ref":"#/$defs/LineChart"},"AreaChart":{"$ref":"#/$defs/AreaChart"},"RadarChart":{"$ref":"#/$defs/RadarChart"},"HorizontalBarChart":{"$ref":"#/$defs/HorizontalBarChart"},"Series":{"$ref":"#/$defs/Series"},"PieChart":{"$ref":"#/$defs/PieChart"},"RadialChart":{"$ref":"#/$defs/RadialChart"},"SingleStackedBarChart":{"$ref":"#/$defs/SingleStackedBarChart"},"Slice":{"$ref":"#/$defs/Slice"},"ScatterChart":{"$ref":"#/$defs/ScatterChart"},"ScatterSeries":{"$ref":"#/$defs/ScatterSeries"},"Point":{"$ref":"#/$defs/Point"},"Form":{"$ref":"#/$defs/Form"},"FormControl":{"$ref":"#/$defs/FormControl"},"Label":{"$ref":"#/$defs/Label"},"Input":{"$ref":"#/$defs/Input"},"TextArea":{"$ref":"#/$defs/TextArea"},"Select":{"$ref":"#/$defs/Select"},"SelectItem":{"$ref":"#/$defs/SelectItem"},"DatePicker":{"$ref":"#/$defs/DatePicker"},"Slider":{"$ref":"#/$defs/Slider"},"CheckBoxGroup":{"$ref":"#/$defs/CheckBoxGroup"},"CheckBoxItem":{"$ref":"#/$defs/CheckBoxItem"},"RadioGroup":{"$ref":"#/$defs/RadioGroup"},"RadioItem":{"$ref":"#/$defs/RadioItem"},"SwitchGroup":{"$ref":"#/$defs/SwitchGroup"},"SwitchItem":{"$ref":"#/$defs/SwitchItem"},"Button":{"$ref":"#/$defs/Button"},"Buttons":{"$ref":"#/$defs/Buttons"},"ListBlock":{"$ref":"#/$defs/ListBlock"},"ListItem":{"$ref":"#/$defs/ListItem"},"FollowUpBlock":{"$ref":"#/$defs/FollowUpBlock"},"FollowUpItem":{"$ref":"#/$defs/FollowUpItem"},"SectionBlock":{"$ref":"#/$defs/SectionBlock"},"SectionItem":{"$ref":"#/$defs/SectionItem"},"Tabs":{"$ref":"#/$defs/Tabs"},"TabItem":{"$ref":"#/$defs/TabItem"},"Accordion":{"$ref":"#/$defs/Accordion"},"AccordionItem":{"$ref":"#/$defs/AccordionItem"},"Steps":{"$ref":"#/$defs/Steps"},"StepsItem":{"$ref":"#/$defs/StepsItem"},"Carousel":{"$ref":"#/$defs/Carousel"},"TagBlock":{"$ref":"#/$defs/TagBlock"},"Tag":{"$ref":"#/$defs/Tag"},"EntityList":{"$ref":"#/$defs/EntityList"},"InlineHeader":{"$ref":"#/$defs/InlineHeader"},"Icon":{"$ref":"#/$defs/Icon"},"IconButton":{"$ref":"#/$defs/IconButton"},"EditableTable":{"$ref":"#/$defs/EditableTable"},"ChipItem":{"$ref":"#/$defs/ChipItem"},"Chips":{"$ref":"#/$defs/Chips"},"OptionCard":{"$ref":"#/$defs/OptionCard"},"OptionCards":{"$ref":"#/$defs/OptionCards"},"Text":{"$ref":"#/$defs/Text"},"BoldText":{"$ref":"#/$defs/BoldText"},"IconText":{"$ref":"#/$defs/IconText"},"ImageText":{"$ref":"#/$defs/ImageText"},"ImageTextLarge":{"$ref":"#/$defs/ImageTextLarge"},"MetricIndicatorInline":{"$ref":"#/$defs/MetricIndicatorInline"},"MetricIndicatorWithStrikethrough":{"$ref":"#/$defs/MetricIndicatorWithStrikethrough"},"SnippetCardItem":{"$ref":"#/$defs/SnippetCardItem"},"SnippetCardBlock":{"$ref":"#/$defs/SnippetCardBlock"},"OverviewCardItem":{"$ref":"#/$defs/OverviewCardItem"},"OverviewCardBlock":{"$ref":"#/$defs/OverviewCardBlock"},"ContextCardItem":{"$ref":"#/$defs/ContextCardItem"},"ContextCardBlock":{"$ref":"#/$defs/ContextCardBlock"},"CompositeCardItem":{"$ref":"#/$defs/CompositeCardItem"},"CompositeCardBlock":{"$ref":"#/$defs/CompositeCardBlock"},"VisualCardItem":{"$ref":"#/$defs/VisualCardItem"},"VisualCardBlock":{"$ref":"#/$defs/VisualCardBlock"}},"required":["Card","CardHeader","TextContent","MarkDownRenderer","Callout","TextCallout","Image","ImageBlock","ImageGallery","CodeBlock","Separator","Table","Col","BarChart","LineChart","AreaChart","RadarChart","HorizontalBarChart","Series","PieChart","RadialChart","SingleStackedBarChart","Slice","ScatterChart","ScatterSeries","Point","Form","FormControl","Label","Input","TextArea","Select","SelectItem","DatePicker","Slider","CheckBoxGroup","CheckBoxItem","RadioGroup","RadioItem","SwitchGroup","SwitchItem","Button","Buttons","ListBlock","ListItem","FollowUpBlock","FollowUpItem","SectionBlock","SectionItem","Tabs","TabItem","Accordion","AccordionItem","Steps","StepsItem","Carousel","TagBlock","Tag","EntityList","InlineHeader","Icon","IconButton","EditableTable","ChipItem","Chips","OptionCard","OptionCards","Text","BoldText","IconText","ImageText","ImageTextLarge","MetricIndicatorInline","MetricIndicatorWithStrikethrough","SnippetCardItem","SnippetCardBlock","OverviewCardItem","OverviewCardBlock","ContextCardItem","ContextCardBlock","CompositeCardItem","CompositeCardBlock","VisualCardItem","VisualCardBlock"],"additionalProperties":false,"$defs":{"Card":{"type":"object","properties":{"children":{"type":"array","items":{"anyOf":[{"$ref":"#/$defs/TextContent"},{"$ref":"#/$defs/MarkDownRenderer"},{"$ref":"#/$defs/CardHeader"},{"$ref":"#/$defs/Callout"},{"$ref":"#/$defs/TextCallout"},{"$ref":"#/$defs/CodeBlock"},{"$ref":"#/$defs/Image"},{"$ref":"#/$defs/ImageBlock"},{"$ref":"#/$defs/ImageGallery"},{"$ref":"#/$defs/Separator"},{"$ref":"#/$defs/HorizontalBarChart"},{"$ref":"#/$defs/RadarChart"},{"$ref":"#/$defs/PieChart"},{"$ref":"#/$defs/RadialChart"},{"$ref":"#/$defs/SingleStackedBarChart"},{"$ref":"#/$defs/ScatterChart"},{"$ref":"#/$defs/AreaChart"},{"$ref":"#/$defs/BarChart"},{"$ref":"#/$defs/LineChart"},{"$ref":"#/$defs/Table"},{"$ref":"#/$defs/TagBlock"},{"$ref":"#/$defs/Form"},{"$ref":"#/$defs/Buttons"},{"$ref":"#/$defs/IconButton"},{"$ref":"#/$defs/Steps"},{"$ref":"#/$defs/InlineHeader"},{"$ref":"#/$defs/EntityList"},{"$ref":"#/$defs/EditableTable"},{"$ref":"#/$defs/SnippetCardBlock"},{"$ref":"#/$defs/OverviewCardBlock"},{"$ref":"#/$defs/ContextCardBlock"},{"$ref":"#/$defs/CompositeCardBlock"},{"$ref":"#/$defs/VisualCardBlock"},{"$ref":"#/$defs/ListBlock"},{"$ref":"#/$defs/FollowUpBlock"},{"$ref":"#/$defs/SectionBlock"},{"$ref":"#/$defs/Tabs"},{"$ref":"#/$defs/Carousel"}]}},"sources":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string"},"title":{"type":"string"},"sourceName":{"type":"string"}},"required":["title","sourceName"],"additionalProperties":false}}},"required":["children"],"additionalProperties":false,"description":"Vertical container for all content in a chat response. Children stack top to bottom automatically. Optional sources ([{ title, sourceName, url }]) render as a Sources strip at the bottom and back inline [n] citations in TextContent."},"TextContent":{"type":"object","properties":{"text":{"type":"string"},"size":{"type":"string","enum":["small","default","large","small-heavy","large-heavy"]}},"required":["text"],"additionalProperties":false,"description":"Text block. Supports markdown. Optional size: \"small\" | \"default\" | \"large\" | \"small-heavy\" | \"large-heavy\"."},"MarkDownRenderer":{"type":"object","properties":{"textMarkdown":{"type":"string"},"variant":{"type":"string","enum":["clear","card","sunk"]}},"required":["textMarkdown"],"additionalProperties":false,"description":"Renders markdown text with optional container variant"},"CardHeader":{"type":"object","properties":{"title":{"type":"string"},"subtitle":{"type":"string"}},"additionalProperties":false,"description":"Header with optional title and subtitle"},"Callout":{"type":"object","properties":{"variant":{"type":"string","enum":["info","warning","error","success","neutral"]},"title":{"type":"string"},"description":{"type":"string"},"visible":{"type":"boolean"}},"required":["variant","title","description"],"additionalProperties":false,"description":"Callout banner. Optional visible is a reactive $boolean — auto-dismisses after 3s by setting $visible to false."},"TextCallout":{"type":"object","properties":{"variant":{"type":"string","enum":["neutral","info","warning","success","danger"]},"title":{"type":"string"},"description":{"type":"string"}},"additionalProperties":false,"description":"Text callout with variant, title, and description"},"CodeBlock":{"type":"object","properties":{"language":{"type":"string"},"codeString":{"type":"string"}},"required":["language","codeString"],"additionalProperties":false,"description":"Syntax-highlighted code block"},"Image":{"type":"object","properties":{"alt":{"type":"string"},"src":{"type":"string"}},"required":["alt"],"additionalProperties":false,"description":"Image with alt text and optional URL"},"ImageBlock":{"type":"object","properties":{"src":{"type":"string"},"alt":{"type":"string"}},"required":["src"],"additionalProperties":false,"description":"Image block with loading state"},"ImageGallery":{"type":"object","properties":{"images":{"type":"array","items":{"type":"object","properties":{"src":{"type":"string"},"alt":{"type":"string"},"details":{"type":"string"}},"required":["src"],"additionalProperties":false}}},"required":["images"],"additionalProperties":false,"description":"Gallery grid of images with modal preview"},"Separator":{"type":"object","properties":{"orientation":{"type":"string","enum":["horizontal","vertical"]},"decorative":{"type":"boolean"}},"additionalProperties":false,"description":"Visual divider between content sections"},"HorizontalBarChart":{"type":"object","properties":{"labels":{"type":"array","items":{"type":"string"}},"series":{"type":"array","items":{"$ref":"#/$defs/Series"}},"variant":{"type":"string","enum":["grouped","stacked"]},"xLabel":{"type":"string"},"yLabel":{"type":"string"}},"required":["labels","series"],"additionalProperties":false,"description":"Horizontal bars; prefer when category labels are long or for ranked lists"},"Series":{"type":"object","properties":{"category":{"type":"string"},"values":{"type":"array","items":{"type":"number"}}},"required":["category","values"],"additionalProperties":false,"description":"One data series"},"RadarChart":{"type":"object","properties":{"labels":{"type":"array","items":{"type":"string"}},"series":{"type":"array","items":{"$ref":"#/$defs/Series"}}},"required":["labels","series"],"additionalProperties":false,"description":"Spider/web chart; use for comparing multiple variables across one or more entities"},"PieChart":{"type":"object","properties":{"labels":{"type":"array","items":{"type":"string"}},"values":{"type":"array","items":{"type":"number"}},"variant":{"type":"string","enum":["pie","donut"]},"appearance":{"type":"string","enum":["circular","semiCircular"]}},"required":["labels","values"],"additionalProperties":false,"description":"Circular slices; use plucked arrays: PieChart(data.categories, data.values)"},"RadialChart":{"type":"object","properties":{"labels":{"type":"array","items":{"type":"string"}},"values":{"type":"array","items":{"type":"number"}}},"required":["labels","values"],"additionalProperties":false,"description":"Radial bars; use plucked arrays: RadialChart(data.categories, data.values)"},"SingleStackedBarChart":{"type":"object","properties":{"labels":{"type":"array","items":{"type":"string"}},"values":{"type":"array","items":{"type":"number"}}},"required":["labels","values"],"additionalProperties":false,"description":"Single horizontal stacked bar; use plucked arrays: SingleStackedBarChart(data.categories, data.values)"},"ScatterChart":{"type":"object","properties":{"datasets":{"type":"array","items":{"$ref":"#/$defs/ScatterSeries"}},"xLabel":{"type":"string"},"yLabel":{"type":"string"}},"required":["datasets"],"additionalProperties":false,"description":"X/Y scatter plot; use for correlations, distributions, and clustering"},"ScatterSeries":{"type":"object","properties":{"name":{"type":"string"},"points":{"type":"array","items":{"$ref":"#/$defs/Point"}}},"required":["name","points"],"additionalProperties":false,"description":"Named dataset"},"Point":{"type":"object","properties":{"x":{"type":"number"},"y":{"type":"number"},"z":{"type":"number"}},"required":["x","y"],"additionalProperties":false,"description":"Data point with numeric coordinates"},"AreaChart":{"type":"object","properties":{"labels":{"type":"array","items":{"type":"string"}},"series":{"type":"array","items":{"$ref":"#/$defs/Series"}},"variant":{"type":"string","enum":["linear","natural","step"]},"xLabel":{"type":"string"},"yLabel":{"type":"string"},"height":{"type":"number"}},"required":["labels","series"],"additionalProperties":false,"description":"Filled area under lines; use for cumulative totals or volume trends over time"},"BarChart":{"type":"object","properties":{"labels":{"type":"array","items":{"type":"string"}},"series":{"type":"array","items":{"$ref":"#/$defs/Series"}},"variant":{"type":"string","enum":["grouped","stacked"]},"xLabel":{"type":"string"},"yLabel":{"type":"string"},"height":{"type":"number"}},"required":["labels","series"],"additionalProperties":false,"description":"Vertical bars; use for comparing values across categories with one or more series"},"LineChart":{"type":"object","properties":{"labels":{"type":"array","items":{"type":"string"}},"series":{"type":"array","items":{"$ref":"#/$defs/Series"}},"variant":{"type":"string","enum":["linear","natural","step"]},"xLabel":{"type":"string"},"yLabel":{"type":"string"},"height":{"type":"number"}},"required":["labels","series"],"additionalProperties":false,"description":"Lines over categories; use for trends and continuous data over time"},"Table":{"type":"object","properties":{"columns":{"type":"array","items":{"$ref":"#/$defs/Col"}}},"required":["columns"],"additionalProperties":false,"description":"Data table — column-oriented. Each Col holds its own data array."},"Col":{"type":"object","properties":{"label":{"type":"string"},"data":{},"type":{"type":"string","enum":["string","number","action"]}},"required":["label","data"],"additionalProperties":false,"description":"Column definition — holds label + data array"},"TagBlock":{"type":"object","properties":{"tags":{"type":"array","items":{"type":"string"}},"size":{"type":"string","enum":["sm","md","lg"]}},"required":["tags"],"additionalProperties":false,"description":"tags is an array of strings; optional size sm | md | lg"},"Form":{"type":"object","properties":{"name":{"type":"string"},"buttons":{"$ref":"#/$defs/Buttons"},"fields":{"default":[],"type":"array","items":{"$ref":"#/$defs/FormControl"}}},"required":["name","buttons","fields"],"additionalProperties":false,"description":"Form container with fields and explicit action buttons"},"Buttons":{"type":"object","properties":{"buttons":{"type":"array","items":{"$ref":"#/$defs/Button"}},"direction":{"type":"string","enum":["row","column"]}},"required":["buttons"],"additionalProperties":false,"description":"Group of Button components. direction: \"row\" (default) | \"column\"."},"Button":{"type":"object","properties":{"label":{"type":"string"},"action":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","const":"open_url"},"url":{"type":"string"}},"required":["type","url"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string","const":"continue_conversation"},"context":{"type":"string"}},"required":["type"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string"},"params":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["type"],"additionalProperties":false}]},"variant":{"type":"string","enum":["primary","secondary","tertiary"]},"type":{"type":"string","enum":["normal","destructive"]},"size":{"type":"string","enum":["extra-small","small","medium","large"]}},"required":["label"],"additionalProperties":false,"description":"Clickable button"},"FormControl":{"type":"object","properties":{"label":{"type":"string"},"input":{"anyOf":[{"$ref":"#/$defs/Input"},{"$ref":"#/$defs/TextArea"},{"$ref":"#/$defs/Select"},{"$ref":"#/$defs/DatePicker"},{"$ref":"#/$defs/Slider"},{"$ref":"#/$defs/CheckBoxGroup"},{"$ref":"#/$defs/RadioGroup"},{"$ref":"#/$defs/Chips"},{"$ref":"#/$defs/OptionCards"}]},"hint":{"type":"string"}},"required":["label","input"],"additionalProperties":false,"description":"Field with label, input component, and optional hint text"},"Input":{"type":"object","properties":{"name":{"type":"string"},"placeholder":{"type":"string"},"type":{"type":"string","enum":["text","email","password","number","url"]},"rules":{"type":"object","properties":{"required":{"type":"boolean"},"email":{"type":"boolean"},"url":{"type":"boolean"},"numeric":{"type":"boolean"},"min":{"type":"number"},"max":{"type":"number"},"minLength":{"type":"number"},"maxLength":{"type":"number"},"pattern":{"type":"string"}},"additionalProperties":false},"value":{"type":"string"}},"required":["name"],"additionalProperties":false},"TextArea":{"type":"object","properties":{"name":{"type":"string"},"placeholder":{"type":"string"},"rows":{"type":"number"},"rules":{"type":"object","properties":{"required":{"type":"boolean"},"email":{"type":"boolean"},"url":{"type":"boolean"},"numeric":{"type":"boolean"},"min":{"type":"number"},"max":{"type":"number"},"minLength":{"type":"number"},"maxLength":{"type":"number"},"pattern":{"type":"string"}},"additionalProperties":false},"value":{"type":"string"}},"required":["name"],"additionalProperties":false},"Select":{"type":"object","properties":{"name":{"type":"string"},"items":{"type":"array","items":{"$ref":"#/$defs/SelectItem"}},"placeholder":{"type":"string"},"rules":{"type":"object","properties":{"required":{"type":"boolean"},"email":{"type":"boolean"},"url":{"type":"boolean"},"numeric":{"type":"boolean"},"min":{"type":"number"},"max":{"type":"number"},"minLength":{"type":"number"},"maxLength":{"type":"number"},"pattern":{"type":"string"}},"additionalProperties":false},"value":{"type":"string"},"size":{"type":"string","enum":["small","medium","large"]}},"required":["name","items"],"additionalProperties":false},"SelectItem":{"type":"object","properties":{"value":{"type":"string"},"label":{"type":"string"}},"required":["value","label"],"additionalProperties":false,"description":"Option for Select"},"DatePicker":{"type":"object","properties":{"name":{"type":"string"},"mode":{"type":"string","enum":["single","range"]},"rules":{"type":"object","properties":{"required":{"type":"boolean"},"email":{"type":"boolean"},"url":{"type":"boolean"},"numeric":{"type":"boolean"},"min":{"type":"number"},"max":{"type":"number"},"minLength":{"type":"number"},"maxLength":{"type":"number"},"pattern":{"type":"string"}},"additionalProperties":false},"value":{}},"required":["name"],"additionalProperties":false},"Slider":{"type":"object","properties":{"name":{"type":"string"},"variant":{"type":"string","enum":["continuous","discrete"]},"min":{"type":"number"},"max":{"type":"number"},"step":{"type":"number"},"defaultValue":{"type":"array","items":{"type":"number"}},"label":{"type":"string"},"rules":{"type":"object","properties":{"required":{"type":"boolean"},"email":{"type":"boolean"},"url":{"type":"boolean"},"numeric":{"type":"boolean"},"min":{"type":"number"},"max":{"type":"number"},"minLength":{"type":"number"},"maxLength":{"type":"number"},"pattern":{"type":"string"}},"additionalProperties":false},"value":{"type":"array","items":{"type":"number"}}},"required":["name","variant","min","max"],"additionalProperties":false,"description":"Numeric slider input; supports continuous and discrete (stepped) variants"},"CheckBoxGroup":{"type":"object","properties":{"name":{"type":"string"},"items":{"type":"array","items":{"$ref":"#/$defs/CheckBoxItem"}},"rules":{"type":"object","properties":{"required":{"type":"boolean"},"email":{"type":"boolean"},"url":{"type":"boolean"},"numeric":{"type":"boolean"},"min":{"type":"number"},"max":{"type":"number"},"minLength":{"type":"number"},"maxLength":{"type":"number"},"pattern":{"type":"string"}},"additionalProperties":false},"value":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"boolean"}}},"required":["name","items"],"additionalProperties":false},"CheckBoxItem":{"type":"object","properties":{"label":{"type":"string"},"description":{"type":"string"},"name":{"type":"string"},"defaultChecked":{"type":"boolean"}},"required":["label","description","name"],"additionalProperties":false},"RadioGroup":{"type":"object","properties":{"name":{"type":"string"},"items":{"type":"array","items":{"$ref":"#/$defs/RadioItem"}},"defaultValue":{"type":"string"},"rules":{"type":"object","properties":{"required":{"type":"boolean"},"email":{"type":"boolean"},"url":{"type":"boolean"},"numeric":{"type":"boolean"},"min":{"type":"number"},"max":{"type":"number"},"minLength":{"type":"number"},"maxLength":{"type":"number"},"pattern":{"type":"string"}},"additionalProperties":false},"value":{"type":"string"}},"required":["name","items"],"additionalProperties":false},"RadioItem":{"type":"object","properties":{"label":{"type":"string"},"description":{"type":"string"},"value":{"type":"string"}},"required":["label","description","value"],"additionalProperties":false},"Chips":{"type":"object","properties":{"name":{"type":"string"},"type":{"default":"multiple","type":"string","enum":["single","multiple"]},"items":{"default":[],"type":"array","items":{"$ref":"#/$defs/ChipItem"}},"rules":{"type":"object","properties":{"required":{"type":"boolean"},"email":{"type":"boolean"},"url":{"type":"boolean"},"numeric":{"type":"boolean"},"min":{"type":"number"},"max":{"type":"number"},"minLength":{"type":"number"},"maxLength":{"type":"number"},"pattern":{"type":"string"}},"additionalProperties":false},"defaultValue":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]}},"required":["name","type","items"],"additionalProperties":false,"description":"A form field of compact selectable chips for choosing one or many short options; the selection is stored under `name`."},"ChipItem":{"type":"object","properties":{"value":{"type":"string"},"label":{"type":"string"},"icon":{"$ref":"#/$defs/Icon"},"disabled":{"type":"boolean"}},"required":["value","label"],"additionalProperties":false,"description":"A single selectable chip inside a Chips group, with a value, label and optional icon."},"Icon":{"type":"object","properties":{"name":{"type":"string"},"category":{"type":"string"}},"required":["name"],"additionalProperties":false,"description":"A lucide icon by kebab-case name (e.g. 'circle-check'). Optional category picks a topical fallback when the name doesn't resolve."},"OptionCards":{"type":"object","properties":{"name":{"type":"string"},"type":{"default":"single","type":"string","enum":["single","multiple"]},"items":{"default":[],"type":"array","items":{"$ref":"#/$defs/OptionCard"}},"rules":{"type":"object","properties":{"required":{"type":"boolean"},"email":{"type":"boolean"},"url":{"type":"boolean"},"numeric":{"type":"boolean"},"min":{"type":"number"},"max":{"type":"number"},"minLength":{"type":"number"},"maxLength":{"type":"number"},"pattern":{"type":"string"}},"additionalProperties":false},"defaultValue":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]}},"required":["name","type","items"],"additionalProperties":false,"description":"A form field of selectable cards (title, optional subtitle, optional icon or image) laid out in a responsive grid for choosing one or many options; the selection is stored under `name`."},"OptionCard":{"type":"object","properties":{"value":{"type":"string"},"title":{"type":"string"},"subtitle":{"type":"string"},"topContent":{"anyOf":[{"$ref":"#/$defs/Icon"},{"$ref":"#/$defs/Image"}]},"disabled":{"type":"boolean"}},"required":["value","title"],"additionalProperties":false,"description":"A single selectable card inside an OptionCards group, with a value, title, optional subtitle and an optional Icon or Image on top."},"IconButton":{"type":"object","properties":{"name":{"type":"string"},"icon":{"$ref":"#/$defs/Icon"},"action":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","const":"open_url"},"url":{"type":"string"}},"required":["type","url"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string","const":"continue_conversation"},"context":{"type":"string"}},"required":["type"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string"},"params":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["type"],"additionalProperties":false}]},"variant":{"type":"string","enum":["primary","secondary","tertiary"]},"size":{"type":"string","enum":["extra-small","small","medium","large"]},"shape":{"type":"string","enum":["square","circle"]}},"required":["name","icon"],"additionalProperties":false,"description":"Icon-only button. name is the accessible label and the action label; icon is an Icon; action fires on click."},"Steps":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/$defs/StepsItem"}}},"required":["items"],"additionalProperties":false,"description":"Step-by-step guide"},"StepsItem":{"type":"object","properties":{"title":{"type":"string"},"details":{"type":"string"}},"required":["title","details"],"additionalProperties":false,"description":"title and details text for one step"},"InlineHeader":{"type":"object","properties":{"heading":{"type":"string"},"description":{"type":"string"}},"required":["heading"],"additionalProperties":false,"description":"Compact section heading with an optional one-line description, for use inside cards."},"EntityList":{"type":"object","properties":{"rows":{"default":[],"type":"array","items":{"type":"object","properties":{"left":{"type":"string"},"right":{"type":"string"},"rightVariant":{"default":"text","type":"string","enum":["text","number"]}},"required":["left","right","rightVariant"],"additionalProperties":false}},"size":{"default":"default","type":"string","enum":["small","default"]},"header":{"type":"object","properties":{"left":{"type":"string"},"right":{"type":"string"},"rightVariant":{"default":"text","type":"string","enum":["text","number"]}},"required":["left","right","rightVariant"],"additionalProperties":false},"footer":{"type":"object","properties":{"left":{"type":"string"},"right":{"type":"string"},"rightVariant":{"default":"text","type":"string","enum":["text","number"]}},"required":["left","right","rightVariant"],"additionalProperties":false}},"required":["rows","size"],"additionalProperties":false,"description":"Two-column key/value rows (left label, right value). size 'default' supports optional header and footer rows; rightVariant 'number' uses tabular numbers."},"EditableTable":{"type":"object","properties":{"name":{"default":"","type":"string"},"columns":{"default":[],"type":"array","items":{"type":"object","properties":{"type":{"type":"string","enum":["text","number","date-single","select","url"]},"key":{"default":"default","type":"string"},"header":{"default":"","type":"string"},"width":{"type":"number"},"options":{"type":"array","items":{"type":"object","properties":{"value":{"type":"string"},"label":{"type":"string"}},"required":["value","label"],"additionalProperties":false}}},"required":["type","key","header"],"additionalProperties":false}},"data":{"default":[],"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"values":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}}},"required":["id","values"],"additionalProperties":false}}},"required":["name","columns","data"],"additionalProperties":false,"description":"Spreadsheet-like table whose cells the user can edit inline (text, number, url, date, select columns); edits are saved back as a form field"},"SnippetCardBlock":{"type":"object","properties":{"items":{"minItems":2,"type":"array","items":{"$ref":"#/$defs/SnippetCardItem"}},"layout":{"default":"grid","type":"string","enum":["grid"]},"responsive":{"default":true,"type":"boolean"},"action":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","const":"open_url"},"url":{"type":"string"}},"required":["type","url"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string","const":"continue_conversation"},"context":{"type":"string"}},"required":["type"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string"},"params":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["type"],"additionalProperties":false}]},"gap":{"anyOf":[{"type":"number"},{"type":"string"}]}},"required":["items","layout","responsive"],"additionalProperties":false,"description":"A responsive grid of compact label/value cards (2 per row) for showing several short facts side by side; optionally clickable with a shared action."},"SnippetCardItem":{"type":"object","properties":{"id":{"type":"string"},"lhs":{"anyOf":[{"$ref":"#/$defs/IconText"},{"$ref":"#/$defs/ImageText"}]},"rhs":{"anyOf":[{"$ref":"#/$defs/Text"},{"$ref":"#/$defs/BoldText"}]}},"required":["lhs"],"additionalProperties":false,"description":"One row-style snippet card: a label on the left (IconText or ImageText) and an optional value on the right (Text or BoldText)."},"IconText":{"type":"object","properties":{"icon":{"$ref":"#/$defs/Icon"},"iconVariant":{"default":"neutral","type":"string","enum":["neutral","info","success","warning","danger","inverted","filled","soft"]},"iconSize":{"default":"m","type":"string","enum":["xs","s","m","l","xl","sm","md","lg"]},"title":{"type":"string"},"subtitle":{"type":"string"},"bold":{"default":false,"type":"boolean"},"layout":{"default":"horizontal","type":"string","enum":["horizontal","vertical"]}},"required":["icon","iconVariant","iconSize","title","bold","layout"],"additionalProperties":false,"description":"An icon badge with a title and optional subtitle, laid out horizontally or vertically. iconVariant sets the badge color."},"ImageText":{"type":"object","properties":{"src":{"type":"string"},"alt":{"type":"string"},"title":{"type":"string"},"subtitle":{"type":"string"},"bold":{"default":false,"type":"boolean"},"layout":{"default":"horizontal","type":"string","enum":["horizontal","vertical"]},"imageSize":{"type":"number"}},"required":["src","title","bold","layout"],"additionalProperties":false,"description":"A small square image (thumbnail/avatar) with a title and optional subtitle. src must be a real image URL."},"Text":{"type":"object","properties":{"variant":{"default":"text","type":"string","enum":["text","number"]},"value":{"type":"string"},"subtext":{"type":"string"},"subtextVariant":{"default":"text","type":"string","enum":["text","number","metric"]},"size":{"default":"sm","type":"string","enum":["xs","sm","md","lg"]}},"required":["variant","value","subtextVariant","size"],"additionalProperties":false,"description":"Plain text line with optional subtext. variant 'number' uses tabular number styling; subtextVariant 'metric' colors a leading +/- subtext green/red."},"BoldText":{"type":"object","properties":{"variant":{"default":"text","type":"string","enum":["text","number"]},"value":{"type":"string"},"subtext":{"type":"string"},"subtextVariant":{"default":"text","type":"string","enum":["text","number","metric"]},"size":{"default":"sm","type":"string","enum":["xs","sm","md","lg"]}},"required":["variant","value","subtextVariant","size"],"additionalProperties":false,"description":"Emphasized (bold) text line with optional subtext. variant 'number' uses tabular number styling; subtextVariant 'metric' colors a leading +/- subtext green/red."},"OverviewCardBlock":{"type":"object","properties":{"items":{"minItems":2,"type":"array","items":{"$ref":"#/$defs/OverviewCardItem"}},"layout":{"default":"grid","type":"string","enum":["grid","carousel"]},"responsive":{"default":true,"type":"boolean"},"action":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","const":"open_url"},"url":{"type":"string"}},"required":["type","url"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string","const":"continue_conversation"},"context":{"type":"string"}},"required":["type"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string"},"params":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["type"],"additionalProperties":false}]},"gap":{"anyOf":[{"type":"number"},{"type":"string"}]}},"required":["items","layout","responsive"],"additionalProperties":false,"description":"A grid or horizontal carousel of compact overview cards, each with a heading (icon/image/text) on top and an inline metric below; optionally clickable with a shared action."},"OverviewCardItem":{"type":"object","properties":{"id":{"type":"string"},"top":{"anyOf":[{"$ref":"#/$defs/IconText"},{"$ref":"#/$defs/ImageText"},{"$ref":"#/$defs/Text"}]},"bottom":{"$ref":"#/$defs/MetricIndicatorInline"}},"required":["top"],"additionalProperties":false,"description":"One overview card: a heading slot at the top (IconText, ImageText or Text) and an optional MetricIndicatorInline at the bottom."},"MetricIndicatorInline":{"type":"object","properties":{"value":{"type":"string"},"subtext":{"type":"string"},"trend":{"type":"object","properties":{"direction":{"type":"string","enum":["up","down"]},"value":{"type":"number"}},"required":["direction","value"],"additionalProperties":false}},"required":["value"],"additionalProperties":false,"description":"Headline metric value with an optional +/- percentage trend and subtext, all on one line."},"ContextCardBlock":{"type":"object","properties":{"items":{"minItems":2,"type":"array","items":{"$ref":"#/$defs/ContextCardItem"}},"layout":{"default":"grid","type":"string","enum":["grid","carousel"]},"responsive":{"default":true,"type":"boolean"},"action":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","const":"open_url"},"url":{"type":"string"}},"required":["type","url"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string","const":"continue_conversation"},"context":{"type":"string"}},"required":["type"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string"},"params":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["type"],"additionalProperties":false}]},"gap":{"anyOf":[{"type":"number"},{"type":"string"}]}},"required":["items","layout","responsive"],"additionalProperties":false,"description":"A grid or carousel of compact tinted context cards (title or tag plus a short bold body); an optional action makes every card clickable."},"ContextCardItem":{"type":"object","properties":{"id":{"type":"string"},"title":{"anyOf":[{"type":"string"},{"$ref":"#/$defs/Tag"}]},"body":{"type":"string"},"bgColor":{"type":"string","enum":["gray"]},"bgImageSrc":{"type":"string"},"bgImageAlt":{"type":"string"}},"required":["title"],"additionalProperties":false,"description":"A single card inside a ContextCardBlock: a title (plain string or Tag), an optional markdown body, and an optional gray tint or background image."},"Tag":{"type":"object","properties":{"text":{"type":"string"},"icon":{"$ref":"#/$defs/Icon"},"size":{"type":"string","enum":["sm","md","lg"]},"variant":{"type":"string","enum":["neutral","info","success","warning","danger"]}},"required":["text"],"additionalProperties":false,"description":"Styled tag/badge with optional Icon and variant"},"CompositeCardBlock":{"type":"object","properties":{"items":{"minItems":2,"type":"array","items":{"$ref":"#/$defs/CompositeCardItem"}},"layout":{"default":"grid","type":"string","enum":["grid","carousel"]},"responsive":{"default":true,"type":"boolean"},"action":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","const":"open_url"},"url":{"type":"string"}},"required":["type","url"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string","const":"continue_conversation"},"context":{"type":"string"}},"required":["type"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string"},"params":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["type"],"additionalProperties":false}]},"gap":{"anyOf":[{"type":"number"},{"type":"string"}]}},"required":["items","layout","responsive"],"additionalProperties":false,"description":"A two-per-row grid or carousel of rich cards, each with an optional header, stacked body content (text, metrics, charts, lists, tags) and a price/button footer; an optional action makes every card clickable."},"CompositeCardItem":{"type":"object","properties":{"id":{"type":"string"},"header":{"anyOf":[{"$ref":"#/$defs/IconText"},{"$ref":"#/$defs/ImageText"},{"$ref":"#/$defs/ImageTextLarge"},{"$ref":"#/$defs/Text"},{"$ref":"#/$defs/Image"}]},"body":{"default":[],"type":"array","items":{"anyOf":[{"$ref":"#/$defs/Text"},{"$ref":"#/$defs/BoldText"},{"$ref":"#/$defs/MetricIndicatorInline"},{"$ref":"#/$defs/IconText"},{"$ref":"#/$defs/Image"},{"$ref":"#/$defs/AreaChart"},{"$ref":"#/$defs/BarChart"},{"$ref":"#/$defs/LineChart"},{"$ref":"#/$defs/ListBlock"},{"$ref":"#/$defs/TagBlock"},{"$ref":"#/$defs/EntityList"}]}},"footer":{"type":"object","properties":{"price":{"anyOf":[{"$ref":"#/$defs/BoldText"},{"$ref":"#/$defs/MetricIndicatorWithStrikethrough"}]},"button":{"$ref":"#/$defs/Button"}},"additionalProperties":false}},"required":["body"],"additionalProperties":false,"description":"A single card inside a CompositeCardBlock: an optional header (icon/image/text), a stack of body elements (text, metrics, charts, lists, tags), and an optional price/button footer."},"ImageTextLarge":{"type":"object","properties":{"src":{"type":"string"},"alt":{"type":"string"},"title":{"type":"string"},"subtitle":{"type":"string"},"bold":{"default":false,"type":"boolean"}},"required":["src","title","bold"],"additionalProperties":false,"description":"A full-width banner image above a bold title and optional subtitle. src must be a real image URL."},"ListBlock":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/$defs/ListItem"}},"variant":{"type":"string","enum":["number","image"]},"size":{"type":"string","enum":["default","small"]}},"required":["items"],"additionalProperties":false,"description":"A list of items with number or image indicators. Each item can optionally have an action. size small renders a compact list."},"ListItem":{"type":"object","properties":{"title":{"type":"string"},"subtitle":{"type":"string"},"image":{"type":"object","properties":{"src":{"type":"string"},"alt":{"type":"string"}},"required":["src","alt"],"additionalProperties":false},"actionLabel":{"type":"string"},"action":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","const":"open_url"},"url":{"type":"string"}},"required":["type","url"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string","const":"continue_conversation"},"context":{"type":"string"}},"required":["type"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string"},"params":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["type"],"additionalProperties":false}]}},"required":["title"],"additionalProperties":false,"description":"Item in a ListBlock — displays a title with an optional subtitle and image. When action is provided, the item becomes clickable."},"MetricIndicatorWithStrikethrough":{"type":"object","properties":{"value":{"type":"string"},"subtext":{"type":"string"},"previousValue":{"type":"string"},"trend":{"type":"object","properties":{"direction":{"type":"string","enum":["up","down"]},"value":{"type":"number"}},"required":["direction","value"],"additionalProperties":false}},"required":["value"],"additionalProperties":false,"description":"Headline metric value with an optional struck-through previousValue, a +/- percentage trend, and subtext below."},"VisualCardBlock":{"type":"object","properties":{"items":{"minItems":2,"type":"array","items":{"$ref":"#/$defs/VisualCardItem"}},"layout":{"default":"grid","type":"string","enum":["grid","carousel"]},"responsive":{"default":true,"type":"boolean"},"action":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","const":"open_url"},"url":{"type":"string"}},"required":["type","url"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string","const":"continue_conversation"},"context":{"type":"string"}},"required":["type"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string"},"params":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["type"],"additionalProperties":false}]},"gap":{"anyOf":[{"type":"number"},{"type":"string"}]}},"required":["items","layout","responsive"],"additionalProperties":false,"description":"A grid or carousel of photo-first cards: a full-bleed background image with a tag on top and a bold text panel at the bottom; an optional action makes every card clickable."},"VisualCardItem":{"type":"object","properties":{"body":{"$ref":"#/$defs/BoldText"},"id":{"type":"string"},"bgImageSrc":{"type":"string"},"tag":{"$ref":"#/$defs/Tag"},"bgImageAlt":{"type":"string"}},"required":["body"],"additionalProperties":false,"description":"A single photo-first card inside a VisualCardBlock: a BoldText body panel, an optional Tag, and a background image (bgImageSrc must be a real URL; bgImageAlt is its alt text)."},"FollowUpBlock":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/$defs/FollowUpItem"}}},"required":["items"],"additionalProperties":false,"description":"List of clickable follow-up suggestions placed at the end of a response"},"FollowUpItem":{"type":"object","properties":{"text":{"type":"string"}},"required":["text"],"additionalProperties":false,"description":"Clickable follow-up suggestion — when clicked, sends text as user message"},"SectionBlock":{"type":"object","properties":{"sections":{"type":"array","items":{"$ref":"#/$defs/SectionItem"}},"isFoldable":{"type":"boolean"}},"required":["sections"],"additionalProperties":false,"description":"Collapsible accordion sections. Auto-opens sections as they stream in. Use SectionItem for each section."},"SectionItem":{"type":"object","properties":{"value":{"type":"string"},"trigger":{"type":"string"},"content":{"type":"array","items":{"anyOf":[{"$ref":"#/$defs/TextContent"},{"$ref":"#/$defs/MarkDownRenderer"},{"$ref":"#/$defs/CardHeader"},{"$ref":"#/$defs/Callout"},{"$ref":"#/$defs/TextCallout"},{"$ref":"#/$defs/CodeBlock"},{"$ref":"#/$defs/Image"},{"$ref":"#/$defs/ImageBlock"},{"$ref":"#/$defs/ImageGallery"},{"$ref":"#/$defs/Separator"},{"$ref":"#/$defs/HorizontalBarChart"},{"$ref":"#/$defs/RadarChart"},{"$ref":"#/$defs/PieChart"},{"$ref":"#/$defs/RadialChart"},{"$ref":"#/$defs/SingleStackedBarChart"},{"$ref":"#/$defs/ScatterChart"},{"$ref":"#/$defs/AreaChart"},{"$ref":"#/$defs/BarChart"},{"$ref":"#/$defs/LineChart"},{"$ref":"#/$defs/Table"},{"$ref":"#/$defs/TagBlock"},{"$ref":"#/$defs/Form"},{"$ref":"#/$defs/Buttons"},{"$ref":"#/$defs/IconButton"},{"$ref":"#/$defs/Steps"},{"$ref":"#/$defs/InlineHeader"},{"$ref":"#/$defs/EntityList"},{"$ref":"#/$defs/EditableTable"},{"$ref":"#/$defs/SnippetCardBlock"},{"$ref":"#/$defs/OverviewCardBlock"},{"$ref":"#/$defs/ContextCardBlock"},{"$ref":"#/$defs/CompositeCardBlock"},{"$ref":"#/$defs/VisualCardBlock"},{"$ref":"#/$defs/ListBlock"},{"$ref":"#/$defs/FollowUpBlock"},{"$ref":"#/$defs/Tabs"},{"$ref":"#/$defs/Accordion"}]}}},"required":["value","trigger","content"],"additionalProperties":false,"description":"Section with a label and collapsible content — used inside SectionBlock"},"Tabs":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/$defs/TabItem"}}},"required":["items"],"additionalProperties":false,"description":"Tabbed container"},"TabItem":{"type":"object","properties":{"value":{"type":"string"},"trigger":{"type":"string"},"content":{"type":"array","items":{"anyOf":[{"$ref":"#/$defs/TextContent"},{"$ref":"#/$defs/MarkDownRenderer"},{"$ref":"#/$defs/CardHeader"},{"$ref":"#/$defs/Callout"},{"$ref":"#/$defs/TextCallout"},{"$ref":"#/$defs/CodeBlock"},{"$ref":"#/$defs/Image"},{"$ref":"#/$defs/ImageBlock"},{"$ref":"#/$defs/ImageGallery"},{"$ref":"#/$defs/Separator"},{"$ref":"#/$defs/HorizontalBarChart"},{"$ref":"#/$defs/RadarChart"},{"$ref":"#/$defs/PieChart"},{"$ref":"#/$defs/RadialChart"},{"$ref":"#/$defs/SingleStackedBarChart"},{"$ref":"#/$defs/ScatterChart"},{"$ref":"#/$defs/AreaChart"},{"$ref":"#/$defs/BarChart"},{"$ref":"#/$defs/LineChart"},{"$ref":"#/$defs/Table"},{"$ref":"#/$defs/TagBlock"},{"$ref":"#/$defs/Form"},{"$ref":"#/$defs/Buttons"},{"$ref":"#/$defs/IconButton"},{"$ref":"#/$defs/Steps"},{"$ref":"#/$defs/InlineHeader"},{"$ref":"#/$defs/EntityList"},{"$ref":"#/$defs/EditableTable"},{"$ref":"#/$defs/SnippetCardBlock"},{"$ref":"#/$defs/OverviewCardBlock"},{"$ref":"#/$defs/ContextCardBlock"},{"$ref":"#/$defs/CompositeCardBlock"},{"$ref":"#/$defs/VisualCardBlock"},{"$ref":"#/$defs/ListBlock"},{"$ref":"#/$defs/FollowUpBlock"},{"$ref":"#/$defs/Accordion"}]}}},"required":["value","trigger","content"],"additionalProperties":false,"description":"value is unique id, trigger is tab label, content is array of components"},"Accordion":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/$defs/AccordionItem"}}},"required":["items"],"additionalProperties":false,"description":"Collapsible sections"},"AccordionItem":{"type":"object","properties":{"value":{"type":"string"},"trigger":{"type":"string"},"content":{"type":"array","items":{"anyOf":[{"$ref":"#/$defs/TextContent"},{"$ref":"#/$defs/MarkDownRenderer"},{"$ref":"#/$defs/CardHeader"},{"$ref":"#/$defs/Callout"},{"$ref":"#/$defs/TextCallout"},{"$ref":"#/$defs/CodeBlock"},{"$ref":"#/$defs/Image"},{"$ref":"#/$defs/ImageBlock"},{"$ref":"#/$defs/ImageGallery"},{"$ref":"#/$defs/Separator"},{"$ref":"#/$defs/HorizontalBarChart"},{"$ref":"#/$defs/RadarChart"},{"$ref":"#/$defs/PieChart"},{"$ref":"#/$defs/RadialChart"},{"$ref":"#/$defs/SingleStackedBarChart"},{"$ref":"#/$defs/ScatterChart"},{"$ref":"#/$defs/AreaChart"},{"$ref":"#/$defs/BarChart"},{"$ref":"#/$defs/LineChart"},{"$ref":"#/$defs/Table"},{"$ref":"#/$defs/TagBlock"},{"$ref":"#/$defs/Form"},{"$ref":"#/$defs/Buttons"},{"$ref":"#/$defs/IconButton"},{"$ref":"#/$defs/Steps"},{"$ref":"#/$defs/InlineHeader"},{"$ref":"#/$defs/EntityList"},{"$ref":"#/$defs/EditableTable"},{"$ref":"#/$defs/SnippetCardBlock"},{"$ref":"#/$defs/OverviewCardBlock"},{"$ref":"#/$defs/ContextCardBlock"},{"$ref":"#/$defs/CompositeCardBlock"},{"$ref":"#/$defs/VisualCardBlock"},{"$ref":"#/$defs/ListBlock"},{"$ref":"#/$defs/FollowUpBlock"}]}}},"required":["value","trigger","content"],"additionalProperties":false,"description":"value is unique id, trigger is section title"},"Carousel":{"type":"object","properties":{"children":{"type":"array","items":{"type":"array","items":{"anyOf":[{"$ref":"#/$defs/TextContent"},{"$ref":"#/$defs/MarkDownRenderer"},{"$ref":"#/$defs/CardHeader"},{"$ref":"#/$defs/Callout"},{"$ref":"#/$defs/TextCallout"},{"$ref":"#/$defs/CodeBlock"},{"$ref":"#/$defs/Image"},{"$ref":"#/$defs/ImageBlock"},{"$ref":"#/$defs/ImageGallery"},{"$ref":"#/$defs/Separator"},{"$ref":"#/$defs/HorizontalBarChart"},{"$ref":"#/$defs/RadarChart"},{"$ref":"#/$defs/PieChart"},{"$ref":"#/$defs/RadialChart"},{"$ref":"#/$defs/SingleStackedBarChart"},{"$ref":"#/$defs/ScatterChart"},{"$ref":"#/$defs/AreaChart"},{"$ref":"#/$defs/BarChart"},{"$ref":"#/$defs/LineChart"},{"$ref":"#/$defs/Table"},{"$ref":"#/$defs/TagBlock"},{"$ref":"#/$defs/Form"},{"$ref":"#/$defs/Buttons"},{"$ref":"#/$defs/IconButton"},{"$ref":"#/$defs/Steps"},{"$ref":"#/$defs/InlineHeader"},{"$ref":"#/$defs/EntityList"},{"$ref":"#/$defs/EditableTable"},{"$ref":"#/$defs/SnippetCardBlock"},{"$ref":"#/$defs/OverviewCardBlock"},{"$ref":"#/$defs/ContextCardBlock"},{"$ref":"#/$defs/CompositeCardBlock"},{"$ref":"#/$defs/VisualCardBlock"},{"$ref":"#/$defs/ListBlock"},{"$ref":"#/$defs/FollowUpBlock"}]}}},"variant":{"type":"string","enum":["card","sunk"]}},"required":["children"],"additionalProperties":false,"description":"Horizontal scrollable carousel"},"Slice":{"type":"object","properties":{"category":{"type":"string"},"value":{"type":"number"}},"required":["category","value"],"additionalProperties":false,"description":"One slice with label and numeric value"},"Label":{"type":"object","properties":{"text":{"type":"string"}},"required":["text"],"additionalProperties":false,"description":"Text label"},"SwitchGroup":{"type":"object","properties":{"name":{"type":"string"},"items":{"type":"array","items":{"$ref":"#/$defs/SwitchItem"}},"variant":{"type":"string","enum":["clear","card","sunk"]},"value":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"boolean"}}},"required":["name","items"],"additionalProperties":false,"description":"Group of switch toggles"},"SwitchItem":{"type":"object","properties":{"label":{"type":"string"},"description":{"type":"string"},"name":{"type":"string"},"defaultChecked":{"type":"boolean"}},"required":["name"],"additionalProperties":false,"description":"Individual switch toggle"}}},"root":"Card","reactive":{"Callout":["visible"],"Input":["value"],"TextArea":["value"],"Select":["value"],"DatePicker":["value"],"Slider":["value"],"CheckBoxGroup":["value"],"RadioGroup":["value"],"SwitchGroup":["value"]}}, +"openui": {"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"Card":{"$ref":"#/$defs/Card"},"CardHeader":{"$ref":"#/$defs/CardHeader"},"TextContent":{"$ref":"#/$defs/TextContent"},"MarkDownRenderer":{"$ref":"#/$defs/MarkDownRenderer"},"Callout":{"$ref":"#/$defs/Callout"},"TextCallout":{"$ref":"#/$defs/TextCallout"},"Image":{"$ref":"#/$defs/Image"},"ImageBlock":{"$ref":"#/$defs/ImageBlock"},"ImageGallery":{"$ref":"#/$defs/ImageGallery"},"CodeBlock":{"$ref":"#/$defs/CodeBlock"},"InlineHeader":{"$ref":"#/$defs/InlineHeader"},"Table":{"$ref":"#/$defs/Table"},"Col":{"$ref":"#/$defs/Col"},"EditableTable":{"$ref":"#/$defs/EditableTable"},"BarChart":{"$ref":"#/$defs/BarChart"},"LineChart":{"$ref":"#/$defs/LineChart"},"AreaChart":{"$ref":"#/$defs/AreaChart"},"RadarChart":{"$ref":"#/$defs/RadarChart"},"HorizontalBarChart":{"$ref":"#/$defs/HorizontalBarChart"},"Series":{"$ref":"#/$defs/Series"},"PieChart":{"$ref":"#/$defs/PieChart"},"RadialChart":{"$ref":"#/$defs/RadialChart"},"SingleStackedBarChart":{"$ref":"#/$defs/SingleStackedBarChart"},"Slice":{"$ref":"#/$defs/Slice"},"ScatterChart":{"$ref":"#/$defs/ScatterChart"},"ScatterSeries":{"$ref":"#/$defs/ScatterSeries"},"Point":{"$ref":"#/$defs/Point"},"Form":{"$ref":"#/$defs/Form"},"FormControl":{"$ref":"#/$defs/FormControl"},"Label":{"$ref":"#/$defs/Label"},"Input":{"$ref":"#/$defs/Input"},"TextArea":{"$ref":"#/$defs/TextArea"},"Select":{"$ref":"#/$defs/Select"},"SelectItem":{"$ref":"#/$defs/SelectItem"},"DatePicker":{"$ref":"#/$defs/DatePicker"},"Slider":{"$ref":"#/$defs/Slider"},"CheckBoxGroup":{"$ref":"#/$defs/CheckBoxGroup"},"CheckBoxItem":{"$ref":"#/$defs/CheckBoxItem"},"RadioGroup":{"$ref":"#/$defs/RadioGroup"},"RadioItem":{"$ref":"#/$defs/RadioItem"},"SwitchGroup":{"$ref":"#/$defs/SwitchGroup"},"SwitchItem":{"$ref":"#/$defs/SwitchItem"},"ChipItem":{"$ref":"#/$defs/ChipItem"},"Chips":{"$ref":"#/$defs/Chips"},"OptionCard":{"$ref":"#/$defs/OptionCard"},"OptionCards":{"$ref":"#/$defs/OptionCards"},"Button":{"$ref":"#/$defs/Button"},"Buttons":{"$ref":"#/$defs/Buttons"},"IconButton":{"$ref":"#/$defs/IconButton"},"Stack":{"$ref":"#/$defs/Stack"},"Tabs":{"$ref":"#/$defs/Tabs"},"TabItem":{"$ref":"#/$defs/TabItem"},"Accordion":{"$ref":"#/$defs/Accordion"},"AccordionItem":{"$ref":"#/$defs/AccordionItem"},"Steps":{"$ref":"#/$defs/Steps"},"StepsItem":{"$ref":"#/$defs/StepsItem"},"Carousel":{"$ref":"#/$defs/Carousel"},"Separator":{"$ref":"#/$defs/Separator"},"TagBlock":{"$ref":"#/$defs/TagBlock"},"Tag":{"$ref":"#/$defs/Tag"},"Icon":{"$ref":"#/$defs/Icon"},"EntityList":{"$ref":"#/$defs/EntityList"},"ListBlock":{"$ref":"#/$defs/ListBlock"},"ListItem":{"$ref":"#/$defs/ListItem"},"Text":{"$ref":"#/$defs/Text"},"BoldText":{"$ref":"#/$defs/BoldText"},"IconText":{"$ref":"#/$defs/IconText"},"ImageText":{"$ref":"#/$defs/ImageText"},"ImageTextLarge":{"$ref":"#/$defs/ImageTextLarge"},"MetricIndicatorInline":{"$ref":"#/$defs/MetricIndicatorInline"},"MetricIndicatorWithStrikethrough":{"$ref":"#/$defs/MetricIndicatorWithStrikethrough"},"SnippetCardItem":{"$ref":"#/$defs/SnippetCardItem"},"SnippetCardBlock":{"$ref":"#/$defs/SnippetCardBlock"},"OverviewCardItem":{"$ref":"#/$defs/OverviewCardItem"},"OverviewCardBlock":{"$ref":"#/$defs/OverviewCardBlock"},"ContextCardItem":{"$ref":"#/$defs/ContextCardItem"},"ContextCardBlock":{"$ref":"#/$defs/ContextCardBlock"},"CompositeCardItem":{"$ref":"#/$defs/CompositeCardItem"},"CompositeCardBlock":{"$ref":"#/$defs/CompositeCardBlock"},"VisualCardItem":{"$ref":"#/$defs/VisualCardItem"},"VisualCardBlock":{"$ref":"#/$defs/VisualCardBlock"},"Modal":{"$ref":"#/$defs/Modal"}},"required":["Card","CardHeader","TextContent","MarkDownRenderer","Callout","TextCallout","Image","ImageBlock","ImageGallery","CodeBlock","InlineHeader","Table","Col","EditableTable","BarChart","LineChart","AreaChart","RadarChart","HorizontalBarChart","Series","PieChart","RadialChart","SingleStackedBarChart","Slice","ScatterChart","ScatterSeries","Point","Form","FormControl","Label","Input","TextArea","Select","SelectItem","DatePicker","Slider","CheckBoxGroup","CheckBoxItem","RadioGroup","RadioItem","SwitchGroup","SwitchItem","ChipItem","Chips","OptionCard","OptionCards","Button","Buttons","IconButton","Stack","Tabs","TabItem","Accordion","AccordionItem","Steps","StepsItem","Carousel","Separator","TagBlock","Tag","Icon","EntityList","ListBlock","ListItem","Text","BoldText","IconText","ImageText","ImageTextLarge","MetricIndicatorInline","MetricIndicatorWithStrikethrough","SnippetCardItem","SnippetCardBlock","OverviewCardItem","OverviewCardBlock","ContextCardItem","ContextCardBlock","CompositeCardItem","CompositeCardBlock","VisualCardItem","VisualCardBlock","Modal"],"additionalProperties":false,"$defs":{"Card":{"type":"object","properties":{"children":{"type":"array","items":{"anyOf":[{"$ref":"#/$defs/TextContent"},{"$ref":"#/$defs/MarkDownRenderer"},{"$ref":"#/$defs/CardHeader"},{"$ref":"#/$defs/Callout"},{"$ref":"#/$defs/TextCallout"},{"$ref":"#/$defs/CodeBlock"},{"$ref":"#/$defs/Image"},{"$ref":"#/$defs/ImageBlock"},{"$ref":"#/$defs/ImageGallery"},{"$ref":"#/$defs/Separator"},{"$ref":"#/$defs/HorizontalBarChart"},{"$ref":"#/$defs/RadarChart"},{"$ref":"#/$defs/PieChart"},{"$ref":"#/$defs/RadialChart"},{"$ref":"#/$defs/SingleStackedBarChart"},{"$ref":"#/$defs/ScatterChart"},{"$ref":"#/$defs/AreaChart"},{"$ref":"#/$defs/BarChart"},{"$ref":"#/$defs/LineChart"},{"$ref":"#/$defs/Table"},{"$ref":"#/$defs/TagBlock"},{"$ref":"#/$defs/Form"},{"$ref":"#/$defs/Buttons"},{"$ref":"#/$defs/IconButton"},{"$ref":"#/$defs/Steps"},{"$ref":"#/$defs/InlineHeader"},{"$ref":"#/$defs/EntityList"},{"$ref":"#/$defs/EditableTable"},{"$ref":"#/$defs/SnippetCardBlock"},{"$ref":"#/$defs/OverviewCardBlock"},{"$ref":"#/$defs/ContextCardBlock"},{"$ref":"#/$defs/CompositeCardBlock"},{"$ref":"#/$defs/VisualCardBlock"},{"$ref":"#/$defs/Tabs"},{"$ref":"#/$defs/Carousel"},{"$ref":"#/$defs/Stack"}]}},"variant":{"type":"string","enum":["card","sunk","clear"]},"direction":{"type":"string","enum":["row","column"]},"gap":{"type":"string","enum":["none","xs","s","m","l","xl","2xl"]},"align":{"type":"string","enum":["start","center","end","stretch","baseline"]},"justify":{"type":"string","enum":["start","center","end","between","around","evenly"]},"wrap":{"type":"boolean"}},"required":["children"],"additionalProperties":false,"description":"Styled container. variant: \"card\" (default, elevated) | \"sunk\" (recessed) | \"clear\" (transparent). Always full width. Accepts all Stack flex params (default: direction \"column\"). Cards flex to share space in row/wrap layouts."},"TextContent":{"type":"object","properties":{"text":{"type":"string"},"size":{"type":"string","enum":["small","default","large","small-heavy","large-heavy"]}},"required":["text"],"additionalProperties":false,"description":"Text block. Supports markdown. Optional size: \"small\" | \"default\" | \"large\" | \"small-heavy\" | \"large-heavy\"."},"MarkDownRenderer":{"type":"object","properties":{"textMarkdown":{"type":"string"},"variant":{"type":"string","enum":["clear","card","sunk"]}},"required":["textMarkdown"],"additionalProperties":false,"description":"Renders markdown text with optional container variant"},"CardHeader":{"type":"object","properties":{"title":{"type":"string"},"subtitle":{"type":"string"}},"additionalProperties":false,"description":"Header with optional title and subtitle"},"Callout":{"type":"object","properties":{"variant":{"type":"string","enum":["info","warning","error","success","neutral"]},"title":{"type":"string"},"description":{"type":"string"},"visible":{"type":"boolean"}},"required":["variant","title","description"],"additionalProperties":false,"description":"Callout banner. Optional visible is a reactive $boolean — auto-dismisses after 3s by setting $visible to false."},"TextCallout":{"type":"object","properties":{"variant":{"type":"string","enum":["neutral","info","warning","success","danger"]},"title":{"type":"string"},"description":{"type":"string"}},"additionalProperties":false,"description":"Text callout with variant, title, and description"},"CodeBlock":{"type":"object","properties":{"language":{"type":"string"},"codeString":{"type":"string"}},"required":["language","codeString"],"additionalProperties":false,"description":"Syntax-highlighted code block"},"Image":{"type":"object","properties":{"alt":{"type":"string"},"src":{"type":"string"}},"required":["alt"],"additionalProperties":false,"description":"Image with alt text and optional URL"},"ImageBlock":{"type":"object","properties":{"src":{"type":"string"},"alt":{"type":"string"}},"required":["src"],"additionalProperties":false,"description":"Image block with loading state"},"ImageGallery":{"type":"object","properties":{"images":{"type":"array","items":{"type":"object","properties":{"src":{"type":"string"},"alt":{"type":"string"},"details":{"type":"string"}},"required":["src"],"additionalProperties":false}}},"required":["images"],"additionalProperties":false,"description":"Gallery grid of images with modal preview"},"Separator":{"type":"object","properties":{"orientation":{"type":"string","enum":["horizontal","vertical"]},"decorative":{"type":"boolean"}},"additionalProperties":false,"description":"Visual divider between content sections"},"HorizontalBarChart":{"type":"object","properties":{"labels":{"type":"array","items":{"type":"string"}},"series":{"type":"array","items":{"$ref":"#/$defs/Series"}},"variant":{"type":"string","enum":["grouped","stacked"]},"xLabel":{"type":"string"},"yLabel":{"type":"string"}},"required":["labels","series"],"additionalProperties":false,"description":"Horizontal bars; prefer when category labels are long or for ranked lists"},"Series":{"type":"object","properties":{"category":{"type":"string"},"values":{"type":"array","items":{"type":"number"}}},"required":["category","values"],"additionalProperties":false,"description":"One data series"},"RadarChart":{"type":"object","properties":{"labels":{"type":"array","items":{"type":"string"}},"series":{"type":"array","items":{"$ref":"#/$defs/Series"}}},"required":["labels","series"],"additionalProperties":false,"description":"Spider/web chart; use for comparing multiple variables across one or more entities"},"PieChart":{"type":"object","properties":{"labels":{"type":"array","items":{"type":"string"}},"values":{"type":"array","items":{"type":"number"}},"variant":{"type":"string","enum":["pie","donut"]},"appearance":{"type":"string","enum":["circular","semiCircular"]}},"required":["labels","values"],"additionalProperties":false,"description":"Circular slices; use plucked arrays: PieChart(data.categories, data.values)"},"RadialChart":{"type":"object","properties":{"labels":{"type":"array","items":{"type":"string"}},"values":{"type":"array","items":{"type":"number"}}},"required":["labels","values"],"additionalProperties":false,"description":"Radial bars; use plucked arrays: RadialChart(data.categories, data.values)"},"SingleStackedBarChart":{"type":"object","properties":{"labels":{"type":"array","items":{"type":"string"}},"values":{"type":"array","items":{"type":"number"}}},"required":["labels","values"],"additionalProperties":false,"description":"Single horizontal stacked bar; use plucked arrays: SingleStackedBarChart(data.categories, data.values)"},"ScatterChart":{"type":"object","properties":{"datasets":{"type":"array","items":{"$ref":"#/$defs/ScatterSeries"}},"xLabel":{"type":"string"},"yLabel":{"type":"string"}},"required":["datasets"],"additionalProperties":false,"description":"X/Y scatter plot; use for correlations, distributions, and clustering"},"ScatterSeries":{"type":"object","properties":{"name":{"type":"string"},"points":{"type":"array","items":{"$ref":"#/$defs/Point"}}},"required":["name","points"],"additionalProperties":false,"description":"Named dataset"},"Point":{"type":"object","properties":{"x":{"type":"number"},"y":{"type":"number"},"z":{"type":"number"}},"required":["x","y"],"additionalProperties":false,"description":"Data point with numeric coordinates"},"AreaChart":{"type":"object","properties":{"labels":{"type":"array","items":{"type":"string"}},"series":{"type":"array","items":{"$ref":"#/$defs/Series"}},"variant":{"type":"string","enum":["linear","natural","step"]},"xLabel":{"type":"string"},"yLabel":{"type":"string"},"height":{"type":"number"}},"required":["labels","series"],"additionalProperties":false,"description":"Filled area under lines; use for cumulative totals or volume trends over time"},"BarChart":{"type":"object","properties":{"labels":{"type":"array","items":{"type":"string"}},"series":{"type":"array","items":{"$ref":"#/$defs/Series"}},"variant":{"type":"string","enum":["grouped","stacked"]},"xLabel":{"type":"string"},"yLabel":{"type":"string"},"height":{"type":"number"}},"required":["labels","series"],"additionalProperties":false,"description":"Vertical bars; use for comparing values across categories with one or more series"},"LineChart":{"type":"object","properties":{"labels":{"type":"array","items":{"type":"string"}},"series":{"type":"array","items":{"$ref":"#/$defs/Series"}},"variant":{"type":"string","enum":["linear","natural","step"]},"xLabel":{"type":"string"},"yLabel":{"type":"string"},"height":{"type":"number"}},"required":["labels","series"],"additionalProperties":false,"description":"Lines over categories; use for trends and continuous data over time"},"Table":{"type":"object","properties":{"columns":{"type":"array","items":{"$ref":"#/$defs/Col"}}},"required":["columns"],"additionalProperties":false,"description":"Data table — column-oriented. Each Col holds its own data array."},"Col":{"type":"object","properties":{"label":{"type":"string"},"data":{},"type":{"type":"string","enum":["string","number","action"]}},"required":["label","data"],"additionalProperties":false,"description":"Column definition — holds label + data array"},"TagBlock":{"type":"object","properties":{"tags":{"type":"array","items":{"type":"string"}},"size":{"type":"string","enum":["sm","md","lg"]}},"required":["tags"],"additionalProperties":false,"description":"tags is an array of strings; optional size sm | md | lg"},"Form":{"type":"object","properties":{"name":{"type":"string"},"buttons":{"$ref":"#/$defs/Buttons"},"fields":{"default":[],"type":"array","items":{"$ref":"#/$defs/FormControl"}}},"required":["name","buttons","fields"],"additionalProperties":false,"description":"Form container with fields and explicit action buttons"},"Buttons":{"type":"object","properties":{"buttons":{"type":"array","items":{"$ref":"#/$defs/Button"}},"direction":{"type":"string","enum":["row","column"]}},"required":["buttons"],"additionalProperties":false,"description":"Group of Button components. direction: \"row\" (default) | \"column\"."},"Button":{"type":"object","properties":{"label":{"type":"string"},"action":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","const":"open_url"},"url":{"type":"string"}},"required":["type","url"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string","const":"continue_conversation"},"context":{"type":"string"}},"required":["type"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string"},"params":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["type"],"additionalProperties":false}]},"variant":{"type":"string","enum":["primary","secondary","tertiary"]},"type":{"type":"string","enum":["normal","destructive"]},"size":{"type":"string","enum":["extra-small","small","medium","large"]}},"required":["label"],"additionalProperties":false,"description":"Clickable button"},"FormControl":{"type":"object","properties":{"label":{"type":"string"},"input":{"anyOf":[{"$ref":"#/$defs/Input"},{"$ref":"#/$defs/TextArea"},{"$ref":"#/$defs/Select"},{"$ref":"#/$defs/DatePicker"},{"$ref":"#/$defs/Slider"},{"$ref":"#/$defs/CheckBoxGroup"},{"$ref":"#/$defs/RadioGroup"},{"$ref":"#/$defs/Chips"},{"$ref":"#/$defs/OptionCards"}]},"hint":{"type":"string"}},"required":["label","input"],"additionalProperties":false,"description":"Field with label, input component, and optional hint text"},"Input":{"type":"object","properties":{"name":{"type":"string"},"placeholder":{"type":"string"},"type":{"type":"string","enum":["text","email","password","number","url"]},"rules":{"type":"object","properties":{"required":{"type":"boolean"},"email":{"type":"boolean"},"url":{"type":"boolean"},"numeric":{"type":"boolean"},"min":{"type":"number"},"max":{"type":"number"},"minLength":{"type":"number"},"maxLength":{"type":"number"},"pattern":{"type":"string"}},"additionalProperties":false},"value":{"type":"string"}},"required":["name"],"additionalProperties":false},"TextArea":{"type":"object","properties":{"name":{"type":"string"},"placeholder":{"type":"string"},"rows":{"type":"number"},"rules":{"type":"object","properties":{"required":{"type":"boolean"},"email":{"type":"boolean"},"url":{"type":"boolean"},"numeric":{"type":"boolean"},"min":{"type":"number"},"max":{"type":"number"},"minLength":{"type":"number"},"maxLength":{"type":"number"},"pattern":{"type":"string"}},"additionalProperties":false},"value":{"type":"string"}},"required":["name"],"additionalProperties":false},"Select":{"type":"object","properties":{"name":{"type":"string"},"items":{"type":"array","items":{"$ref":"#/$defs/SelectItem"}},"placeholder":{"type":"string"},"rules":{"type":"object","properties":{"required":{"type":"boolean"},"email":{"type":"boolean"},"url":{"type":"boolean"},"numeric":{"type":"boolean"},"min":{"type":"number"},"max":{"type":"number"},"minLength":{"type":"number"},"maxLength":{"type":"number"},"pattern":{"type":"string"}},"additionalProperties":false},"value":{"type":"string"},"size":{"type":"string","enum":["small","medium","large"]}},"required":["name","items"],"additionalProperties":false},"SelectItem":{"type":"object","properties":{"value":{"type":"string"},"label":{"type":"string"}},"required":["value","label"],"additionalProperties":false,"description":"Option for Select"},"DatePicker":{"type":"object","properties":{"name":{"type":"string"},"mode":{"type":"string","enum":["single","range"]},"rules":{"type":"object","properties":{"required":{"type":"boolean"},"email":{"type":"boolean"},"url":{"type":"boolean"},"numeric":{"type":"boolean"},"min":{"type":"number"},"max":{"type":"number"},"minLength":{"type":"number"},"maxLength":{"type":"number"},"pattern":{"type":"string"}},"additionalProperties":false},"value":{}},"required":["name"],"additionalProperties":false},"Slider":{"type":"object","properties":{"name":{"type":"string"},"variant":{"type":"string","enum":["continuous","discrete"]},"min":{"type":"number"},"max":{"type":"number"},"step":{"type":"number"},"defaultValue":{"type":"array","items":{"type":"number"}},"label":{"type":"string"},"rules":{"type":"object","properties":{"required":{"type":"boolean"},"email":{"type":"boolean"},"url":{"type":"boolean"},"numeric":{"type":"boolean"},"min":{"type":"number"},"max":{"type":"number"},"minLength":{"type":"number"},"maxLength":{"type":"number"},"pattern":{"type":"string"}},"additionalProperties":false},"value":{"type":"array","items":{"type":"number"}}},"required":["name","variant","min","max"],"additionalProperties":false,"description":"Numeric slider input; supports continuous and discrete (stepped) variants"},"CheckBoxGroup":{"type":"object","properties":{"name":{"type":"string"},"items":{"type":"array","items":{"$ref":"#/$defs/CheckBoxItem"}},"rules":{"type":"object","properties":{"required":{"type":"boolean"},"email":{"type":"boolean"},"url":{"type":"boolean"},"numeric":{"type":"boolean"},"min":{"type":"number"},"max":{"type":"number"},"minLength":{"type":"number"},"maxLength":{"type":"number"},"pattern":{"type":"string"}},"additionalProperties":false},"value":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"boolean"}}},"required":["name","items"],"additionalProperties":false},"CheckBoxItem":{"type":"object","properties":{"label":{"type":"string"},"description":{"type":"string"},"name":{"type":"string"},"defaultChecked":{"type":"boolean"}},"required":["label","description","name"],"additionalProperties":false},"RadioGroup":{"type":"object","properties":{"name":{"type":"string"},"items":{"type":"array","items":{"$ref":"#/$defs/RadioItem"}},"defaultValue":{"type":"string"},"rules":{"type":"object","properties":{"required":{"type":"boolean"},"email":{"type":"boolean"},"url":{"type":"boolean"},"numeric":{"type":"boolean"},"min":{"type":"number"},"max":{"type":"number"},"minLength":{"type":"number"},"maxLength":{"type":"number"},"pattern":{"type":"string"}},"additionalProperties":false},"value":{"type":"string"}},"required":["name","items"],"additionalProperties":false},"RadioItem":{"type":"object","properties":{"label":{"type":"string"},"description":{"type":"string"},"value":{"type":"string"}},"required":["label","description","value"],"additionalProperties":false},"Chips":{"type":"object","properties":{"name":{"type":"string"},"type":{"default":"multiple","type":"string","enum":["single","multiple"]},"items":{"default":[],"type":"array","items":{"$ref":"#/$defs/ChipItem"}},"rules":{"type":"object","properties":{"required":{"type":"boolean"},"email":{"type":"boolean"},"url":{"type":"boolean"},"numeric":{"type":"boolean"},"min":{"type":"number"},"max":{"type":"number"},"minLength":{"type":"number"},"maxLength":{"type":"number"},"pattern":{"type":"string"}},"additionalProperties":false},"defaultValue":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]}},"required":["name","type","items"],"additionalProperties":false,"description":"A form field of compact selectable chips for choosing one or many short options; the selection is stored under `name`."},"ChipItem":{"type":"object","properties":{"value":{"type":"string"},"label":{"type":"string"},"icon":{"$ref":"#/$defs/Icon"},"disabled":{"type":"boolean"}},"required":["value","label"],"additionalProperties":false,"description":"A single selectable chip inside a Chips group, with a value, label and optional icon."},"Icon":{"type":"object","properties":{"name":{"type":"string"},"category":{"type":"string"}},"required":["name"],"additionalProperties":false,"description":"A lucide icon by kebab-case name (e.g. 'circle-check'). Optional category picks a topical fallback when the name doesn't resolve."},"OptionCards":{"type":"object","properties":{"name":{"type":"string"},"type":{"default":"single","type":"string","enum":["single","multiple"]},"items":{"default":[],"type":"array","items":{"$ref":"#/$defs/OptionCard"}},"rules":{"type":"object","properties":{"required":{"type":"boolean"},"email":{"type":"boolean"},"url":{"type":"boolean"},"numeric":{"type":"boolean"},"min":{"type":"number"},"max":{"type":"number"},"minLength":{"type":"number"},"maxLength":{"type":"number"},"pattern":{"type":"string"}},"additionalProperties":false},"defaultValue":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]}},"required":["name","type","items"],"additionalProperties":false,"description":"A form field of selectable cards (title, optional subtitle, optional icon or image) laid out in a responsive grid for choosing one or many options; the selection is stored under `name`."},"OptionCard":{"type":"object","properties":{"value":{"type":"string"},"title":{"type":"string"},"subtitle":{"type":"string"},"topContent":{"anyOf":[{"$ref":"#/$defs/Icon"},{"$ref":"#/$defs/Image"}]},"disabled":{"type":"boolean"}},"required":["value","title"],"additionalProperties":false,"description":"A single selectable card inside an OptionCards group, with a value, title, optional subtitle and an optional Icon or Image on top."},"IconButton":{"type":"object","properties":{"name":{"type":"string"},"icon":{"$ref":"#/$defs/Icon"},"action":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","const":"open_url"},"url":{"type":"string"}},"required":["type","url"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string","const":"continue_conversation"},"context":{"type":"string"}},"required":["type"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string"},"params":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["type"],"additionalProperties":false}]},"variant":{"type":"string","enum":["primary","secondary","tertiary"]},"size":{"type":"string","enum":["extra-small","small","medium","large"]},"shape":{"type":"string","enum":["square","circle"]}},"required":["name","icon"],"additionalProperties":false,"description":"Icon-only button. name is the accessible label and the action label; icon is an Icon; action fires on click."},"Steps":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/$defs/StepsItem"}}},"required":["items"],"additionalProperties":false,"description":"Step-by-step guide"},"StepsItem":{"type":"object","properties":{"title":{"type":"string"},"details":{"type":"string"}},"required":["title","details"],"additionalProperties":false,"description":"title and details text for one step"},"InlineHeader":{"type":"object","properties":{"heading":{"type":"string"},"description":{"type":"string"}},"required":["heading"],"additionalProperties":false,"description":"Compact section heading with an optional one-line description, for use inside cards."},"EntityList":{"type":"object","properties":{"rows":{"default":[],"type":"array","items":{"type":"object","properties":{"left":{"type":"string"},"right":{"type":"string"},"rightVariant":{"default":"text","type":"string","enum":["text","number"]}},"required":["left","right","rightVariant"],"additionalProperties":false}},"size":{"default":"default","type":"string","enum":["small","default"]},"header":{"type":"object","properties":{"left":{"type":"string"},"right":{"type":"string"},"rightVariant":{"default":"text","type":"string","enum":["text","number"]}},"required":["left","right","rightVariant"],"additionalProperties":false},"footer":{"type":"object","properties":{"left":{"type":"string"},"right":{"type":"string"},"rightVariant":{"default":"text","type":"string","enum":["text","number"]}},"required":["left","right","rightVariant"],"additionalProperties":false}},"required":["rows","size"],"additionalProperties":false,"description":"Two-column key/value rows (left label, right value). size 'default' supports optional header and footer rows; rightVariant 'number' uses tabular numbers."},"EditableTable":{"type":"object","properties":{"name":{"default":"","type":"string"},"columns":{"default":[],"type":"array","items":{"type":"object","properties":{"type":{"type":"string","enum":["text","number","date-single","select","url"]},"key":{"default":"default","type":"string"},"header":{"default":"","type":"string"},"width":{"type":"number"},"options":{"type":"array","items":{"type":"object","properties":{"value":{"type":"string"},"label":{"type":"string"}},"required":["value","label"],"additionalProperties":false}}},"required":["type","key","header"],"additionalProperties":false}},"data":{"default":[],"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"values":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}}},"required":["id","values"],"additionalProperties":false}}},"required":["name","columns","data"],"additionalProperties":false,"description":"Spreadsheet-like table whose cells the user can edit inline (text, number, url, date, select columns); edits are saved back as a form field"},"SnippetCardBlock":{"type":"object","properties":{"items":{"minItems":2,"type":"array","items":{"$ref":"#/$defs/SnippetCardItem"}},"layout":{"default":"grid","type":"string","enum":["grid"]},"responsive":{"default":true,"type":"boolean"},"action":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","const":"open_url"},"url":{"type":"string"}},"required":["type","url"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string","const":"continue_conversation"},"context":{"type":"string"}},"required":["type"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string"},"params":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["type"],"additionalProperties":false}]},"gap":{"anyOf":[{"type":"number"},{"type":"string"}]}},"required":["items","layout","responsive"],"additionalProperties":false,"description":"A responsive grid of compact label/value cards (2 per row) for showing several short facts side by side; optionally clickable with a shared action."},"SnippetCardItem":{"type":"object","properties":{"id":{"type":"string"},"lhs":{"anyOf":[{"$ref":"#/$defs/IconText"},{"$ref":"#/$defs/ImageText"}]},"rhs":{"anyOf":[{"$ref":"#/$defs/Text"},{"$ref":"#/$defs/BoldText"}]}},"required":["lhs"],"additionalProperties":false,"description":"One row-style snippet card: a label on the left (IconText or ImageText) and an optional value on the right (Text or BoldText)."},"IconText":{"type":"object","properties":{"icon":{"$ref":"#/$defs/Icon"},"iconVariant":{"default":"neutral","type":"string","enum":["neutral","info","success","warning","danger","inverted","filled","soft"]},"iconSize":{"default":"m","type":"string","enum":["xs","s","m","l","xl","sm","md","lg"]},"title":{"type":"string"},"subtitle":{"type":"string"},"bold":{"default":false,"type":"boolean"},"layout":{"default":"horizontal","type":"string","enum":["horizontal","vertical"]}},"required":["icon","iconVariant","iconSize","title","bold","layout"],"additionalProperties":false,"description":"An icon badge with a title and optional subtitle, laid out horizontally or vertically. iconVariant sets the badge color."},"ImageText":{"type":"object","properties":{"src":{"type":"string"},"alt":{"type":"string"},"title":{"type":"string"},"subtitle":{"type":"string"},"bold":{"default":false,"type":"boolean"},"layout":{"default":"horizontal","type":"string","enum":["horizontal","vertical"]},"imageSize":{"type":"number"}},"required":["src","title","bold","layout"],"additionalProperties":false,"description":"A small square image (thumbnail/avatar) with a title and optional subtitle. src must be a real image URL."},"Text":{"type":"object","properties":{"variant":{"default":"text","type":"string","enum":["text","number"]},"value":{"type":"string"},"subtext":{"type":"string"},"subtextVariant":{"default":"text","type":"string","enum":["text","number","metric"]},"size":{"default":"sm","type":"string","enum":["xs","sm","md","lg"]}},"required":["variant","value","subtextVariant","size"],"additionalProperties":false,"description":"Plain text line with optional subtext. variant 'number' uses tabular number styling; subtextVariant 'metric' colors a leading +/- subtext green/red."},"BoldText":{"type":"object","properties":{"variant":{"default":"text","type":"string","enum":["text","number"]},"value":{"type":"string"},"subtext":{"type":"string"},"subtextVariant":{"default":"text","type":"string","enum":["text","number","metric"]},"size":{"default":"sm","type":"string","enum":["xs","sm","md","lg"]}},"required":["variant","value","subtextVariant","size"],"additionalProperties":false,"description":"Emphasized (bold) text line with optional subtext. variant 'number' uses tabular number styling; subtextVariant 'metric' colors a leading +/- subtext green/red."},"OverviewCardBlock":{"type":"object","properties":{"items":{"minItems":2,"type":"array","items":{"$ref":"#/$defs/OverviewCardItem"}},"layout":{"default":"grid","type":"string","enum":["grid","carousel"]},"responsive":{"default":true,"type":"boolean"},"action":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","const":"open_url"},"url":{"type":"string"}},"required":["type","url"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string","const":"continue_conversation"},"context":{"type":"string"}},"required":["type"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string"},"params":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["type"],"additionalProperties":false}]},"gap":{"anyOf":[{"type":"number"},{"type":"string"}]}},"required":["items","layout","responsive"],"additionalProperties":false,"description":"A grid or horizontal carousel of compact overview cards, each with a heading (icon/image/text) on top and an inline metric below; optionally clickable with a shared action."},"OverviewCardItem":{"type":"object","properties":{"id":{"type":"string"},"top":{"anyOf":[{"$ref":"#/$defs/IconText"},{"$ref":"#/$defs/ImageText"},{"$ref":"#/$defs/Text"}]},"bottom":{"$ref":"#/$defs/MetricIndicatorInline"}},"required":["top"],"additionalProperties":false,"description":"One overview card: a heading slot at the top (IconText, ImageText or Text) and an optional MetricIndicatorInline at the bottom."},"MetricIndicatorInline":{"type":"object","properties":{"value":{"type":"string"},"subtext":{"type":"string"},"trend":{"type":"object","properties":{"direction":{"type":"string","enum":["up","down"]},"value":{"type":"number"}},"required":["direction","value"],"additionalProperties":false}},"required":["value"],"additionalProperties":false,"description":"Headline metric value with an optional +/- percentage trend and subtext, all on one line."},"ContextCardBlock":{"type":"object","properties":{"items":{"minItems":2,"type":"array","items":{"$ref":"#/$defs/ContextCardItem"}},"layout":{"default":"grid","type":"string","enum":["grid","carousel"]},"responsive":{"default":true,"type":"boolean"},"action":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","const":"open_url"},"url":{"type":"string"}},"required":["type","url"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string","const":"continue_conversation"},"context":{"type":"string"}},"required":["type"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string"},"params":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["type"],"additionalProperties":false}]},"gap":{"anyOf":[{"type":"number"},{"type":"string"}]}},"required":["items","layout","responsive"],"additionalProperties":false,"description":"A grid or carousel of compact tinted context cards (title or tag plus a short bold body); an optional action makes every card clickable."},"ContextCardItem":{"type":"object","properties":{"id":{"type":"string"},"title":{"anyOf":[{"type":"string"},{"$ref":"#/$defs/Tag"}]},"body":{"type":"string"},"bgColor":{"type":"string","enum":["gray"]},"bgImageSrc":{"type":"string"},"bgImageAlt":{"type":"string"}},"required":["title"],"additionalProperties":false,"description":"A single card inside a ContextCardBlock: a title (plain string or Tag), an optional markdown body, and an optional gray tint or background image."},"Tag":{"type":"object","properties":{"text":{"type":"string"},"icon":{"$ref":"#/$defs/Icon"},"size":{"type":"string","enum":["sm","md","lg"]},"variant":{"type":"string","enum":["neutral","info","success","warning","danger"]}},"required":["text"],"additionalProperties":false,"description":"Styled tag/badge with optional Icon and variant"},"CompositeCardBlock":{"type":"object","properties":{"items":{"minItems":2,"type":"array","items":{"$ref":"#/$defs/CompositeCardItem"}},"layout":{"default":"grid","type":"string","enum":["grid","carousel"]},"responsive":{"default":true,"type":"boolean"},"action":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","const":"open_url"},"url":{"type":"string"}},"required":["type","url"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string","const":"continue_conversation"},"context":{"type":"string"}},"required":["type"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string"},"params":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["type"],"additionalProperties":false}]},"gap":{"anyOf":[{"type":"number"},{"type":"string"}]}},"required":["items","layout","responsive"],"additionalProperties":false,"description":"A two-per-row grid or carousel of rich cards, each with an optional header, stacked body content (text, metrics, charts, lists, tags) and a price/button footer; an optional action makes every card clickable."},"CompositeCardItem":{"type":"object","properties":{"id":{"type":"string"},"header":{"anyOf":[{"$ref":"#/$defs/IconText"},{"$ref":"#/$defs/ImageText"},{"$ref":"#/$defs/ImageTextLarge"},{"$ref":"#/$defs/Text"},{"$ref":"#/$defs/Image"}]},"body":{"default":[],"type":"array","items":{"anyOf":[{"$ref":"#/$defs/Text"},{"$ref":"#/$defs/BoldText"},{"$ref":"#/$defs/MetricIndicatorInline"},{"$ref":"#/$defs/IconText"},{"$ref":"#/$defs/Image"},{"$ref":"#/$defs/AreaChart"},{"$ref":"#/$defs/BarChart"},{"$ref":"#/$defs/LineChart"},{"$ref":"#/$defs/ListBlock"},{"$ref":"#/$defs/TagBlock"},{"$ref":"#/$defs/EntityList"}]}},"footer":{"type":"object","properties":{"price":{"anyOf":[{"$ref":"#/$defs/BoldText"},{"$ref":"#/$defs/MetricIndicatorWithStrikethrough"}]},"button":{"$ref":"#/$defs/Button"}},"additionalProperties":false}},"required":["body"],"additionalProperties":false,"description":"A single card inside a CompositeCardBlock: an optional header (icon/image/text), a stack of body elements (text, metrics, charts, lists, tags), and an optional price/button footer."},"ImageTextLarge":{"type":"object","properties":{"src":{"type":"string"},"alt":{"type":"string"},"title":{"type":"string"},"subtitle":{"type":"string"},"bold":{"default":false,"type":"boolean"}},"required":["src","title","bold"],"additionalProperties":false,"description":"A full-width banner image above a bold title and optional subtitle. src must be a real image URL."},"ListBlock":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/$defs/ListItem"}},"variant":{"type":"string","enum":["number","image"]},"size":{"type":"string","enum":["default","small"]}},"required":["items"],"additionalProperties":false,"description":"A list of items with number or image indicators. Each item can optionally have an action. size small renders a compact list."},"ListItem":{"type":"object","properties":{"title":{"type":"string"},"subtitle":{"type":"string"},"image":{"type":"object","properties":{"src":{"type":"string"},"alt":{"type":"string"}},"required":["src","alt"],"additionalProperties":false},"actionLabel":{"type":"string"},"action":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","const":"open_url"},"url":{"type":"string"}},"required":["type","url"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string","const":"continue_conversation"},"context":{"type":"string"}},"required":["type"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string"},"params":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["type"],"additionalProperties":false}]}},"required":["title"],"additionalProperties":false,"description":"Item in a ListBlock — displays a title with an optional subtitle and image. When action is provided, the item becomes clickable."},"MetricIndicatorWithStrikethrough":{"type":"object","properties":{"value":{"type":"string"},"subtext":{"type":"string"},"previousValue":{"type":"string"},"trend":{"type":"object","properties":{"direction":{"type":"string","enum":["up","down"]},"value":{"type":"number"}},"required":["direction","value"],"additionalProperties":false}},"required":["value"],"additionalProperties":false,"description":"Headline metric value with an optional struck-through previousValue, a +/- percentage trend, and subtext below."},"VisualCardBlock":{"type":"object","properties":{"items":{"minItems":2,"type":"array","items":{"$ref":"#/$defs/VisualCardItem"}},"layout":{"default":"grid","type":"string","enum":["grid","carousel"]},"responsive":{"default":true,"type":"boolean"},"action":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","const":"open_url"},"url":{"type":"string"}},"required":["type","url"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string","const":"continue_conversation"},"context":{"type":"string"}},"required":["type"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string"},"params":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["type"],"additionalProperties":false}]},"gap":{"anyOf":[{"type":"number"},{"type":"string"}]}},"required":["items","layout","responsive"],"additionalProperties":false,"description":"A grid or carousel of photo-first cards: a full-bleed background image with a tag on top and a bold text panel at the bottom; an optional action makes every card clickable."},"VisualCardItem":{"type":"object","properties":{"body":{"$ref":"#/$defs/BoldText"},"id":{"type":"string"},"bgImageSrc":{"type":"string"},"tag":{"$ref":"#/$defs/Tag"},"bgImageAlt":{"type":"string"}},"required":["body"],"additionalProperties":false,"description":"A single photo-first card inside a VisualCardBlock: a BoldText body panel, an optional Tag, and a background image (bgImageSrc must be a real URL; bgImageAlt is its alt text)."},"Tabs":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/$defs/TabItem"}}},"required":["items"],"additionalProperties":false,"description":"Tabbed container"},"TabItem":{"type":"object","properties":{"value":{"type":"string"},"trigger":{"type":"string"},"content":{"type":"array","items":{"anyOf":[{"$ref":"#/$defs/TextContent"},{"$ref":"#/$defs/MarkDownRenderer"},{"$ref":"#/$defs/CardHeader"},{"$ref":"#/$defs/Callout"},{"$ref":"#/$defs/TextCallout"},{"$ref":"#/$defs/CodeBlock"},{"$ref":"#/$defs/Image"},{"$ref":"#/$defs/ImageBlock"},{"$ref":"#/$defs/ImageGallery"},{"$ref":"#/$defs/Separator"},{"$ref":"#/$defs/HorizontalBarChart"},{"$ref":"#/$defs/RadarChart"},{"$ref":"#/$defs/PieChart"},{"$ref":"#/$defs/RadialChart"},{"$ref":"#/$defs/SingleStackedBarChart"},{"$ref":"#/$defs/ScatterChart"},{"$ref":"#/$defs/AreaChart"},{"$ref":"#/$defs/BarChart"},{"$ref":"#/$defs/LineChart"},{"$ref":"#/$defs/Table"},{"$ref":"#/$defs/TagBlock"},{"$ref":"#/$defs/Form"},{"$ref":"#/$defs/Buttons"},{"$ref":"#/$defs/IconButton"},{"$ref":"#/$defs/Steps"},{"$ref":"#/$defs/InlineHeader"},{"$ref":"#/$defs/EntityList"},{"$ref":"#/$defs/EditableTable"},{"$ref":"#/$defs/SnippetCardBlock"},{"$ref":"#/$defs/OverviewCardBlock"},{"$ref":"#/$defs/ContextCardBlock"},{"$ref":"#/$defs/CompositeCardBlock"},{"$ref":"#/$defs/VisualCardBlock"}]}}},"required":["value","trigger","content"],"additionalProperties":false,"description":"value is unique id, trigger is tab label, content is array of components"},"Carousel":{"type":"object","properties":{"children":{"type":"array","items":{"type":"array","items":{"anyOf":[{"$ref":"#/$defs/TextContent"},{"$ref":"#/$defs/MarkDownRenderer"},{"$ref":"#/$defs/CardHeader"},{"$ref":"#/$defs/Callout"},{"$ref":"#/$defs/TextCallout"},{"$ref":"#/$defs/CodeBlock"},{"$ref":"#/$defs/Image"},{"$ref":"#/$defs/ImageBlock"},{"$ref":"#/$defs/ImageGallery"},{"$ref":"#/$defs/Separator"},{"$ref":"#/$defs/HorizontalBarChart"},{"$ref":"#/$defs/RadarChart"},{"$ref":"#/$defs/PieChart"},{"$ref":"#/$defs/RadialChart"},{"$ref":"#/$defs/SingleStackedBarChart"},{"$ref":"#/$defs/ScatterChart"},{"$ref":"#/$defs/AreaChart"},{"$ref":"#/$defs/BarChart"},{"$ref":"#/$defs/LineChart"},{"$ref":"#/$defs/Table"},{"$ref":"#/$defs/TagBlock"},{"$ref":"#/$defs/Form"},{"$ref":"#/$defs/Buttons"},{"$ref":"#/$defs/IconButton"},{"$ref":"#/$defs/Steps"},{"$ref":"#/$defs/InlineHeader"},{"$ref":"#/$defs/EntityList"},{"$ref":"#/$defs/EditableTable"},{"$ref":"#/$defs/SnippetCardBlock"},{"$ref":"#/$defs/OverviewCardBlock"},{"$ref":"#/$defs/ContextCardBlock"},{"$ref":"#/$defs/CompositeCardBlock"},{"$ref":"#/$defs/VisualCardBlock"}]}}},"variant":{"type":"string","enum":["card","sunk"]}},"required":["children"],"additionalProperties":false,"description":"Horizontal scrollable carousel"},"Stack":{"type":"object","properties":{"children":{"type":"array","items":{}},"direction":{"type":"string","enum":["row","column"]},"gap":{"type":"string","enum":["none","xs","s","m","l","xl","2xl"]},"align":{"type":"string","enum":["start","center","end","stretch","baseline"]},"justify":{"type":"string","enum":["start","center","end","between","around","evenly"]},"wrap":{"type":"boolean"}},"required":["children"],"additionalProperties":false,"description":"Flex container. direction: \"row\"|\"column\" (default \"column\"). gap: \"none\"|\"xs\"|\"s\"|\"m\"|\"l\"|\"xl\"|\"2xl\" (default \"m\"). align: \"start\"|\"center\"|\"end\"|\"stretch\"|\"baseline\". justify: \"start\"|\"center\"|\"end\"|\"between\"|\"around\"|\"evenly\"."},"Slice":{"type":"object","properties":{"category":{"type":"string"},"value":{"type":"number"}},"required":["category","value"],"additionalProperties":false,"description":"One slice with label and numeric value"},"Label":{"type":"object","properties":{"text":{"type":"string"}},"required":["text"],"additionalProperties":false,"description":"Text label"},"SwitchGroup":{"type":"object","properties":{"name":{"type":"string"},"items":{"type":"array","items":{"$ref":"#/$defs/SwitchItem"}},"variant":{"type":"string","enum":["clear","card","sunk"]},"value":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"boolean"}}},"required":["name","items"],"additionalProperties":false,"description":"Group of switch toggles"},"SwitchItem":{"type":"object","properties":{"label":{"type":"string"},"description":{"type":"string"},"name":{"type":"string"},"defaultChecked":{"type":"boolean"}},"required":["name"],"additionalProperties":false,"description":"Individual switch toggle"},"Accordion":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/$defs/AccordionItem"}}},"required":["items"],"additionalProperties":false,"description":"Collapsible sections"},"AccordionItem":{"type":"object","properties":{"value":{"type":"string"},"trigger":{"type":"string"},"content":{"type":"array","items":{"anyOf":[{"$ref":"#/$defs/TextContent"},{"$ref":"#/$defs/MarkDownRenderer"},{"$ref":"#/$defs/CardHeader"},{"$ref":"#/$defs/Callout"},{"$ref":"#/$defs/TextCallout"},{"$ref":"#/$defs/CodeBlock"},{"$ref":"#/$defs/Image"},{"$ref":"#/$defs/ImageBlock"},{"$ref":"#/$defs/ImageGallery"},{"$ref":"#/$defs/Separator"},{"$ref":"#/$defs/HorizontalBarChart"},{"$ref":"#/$defs/RadarChart"},{"$ref":"#/$defs/PieChart"},{"$ref":"#/$defs/RadialChart"},{"$ref":"#/$defs/SingleStackedBarChart"},{"$ref":"#/$defs/ScatterChart"},{"$ref":"#/$defs/AreaChart"},{"$ref":"#/$defs/BarChart"},{"$ref":"#/$defs/LineChart"},{"$ref":"#/$defs/Table"},{"$ref":"#/$defs/TagBlock"},{"$ref":"#/$defs/Form"},{"$ref":"#/$defs/Buttons"},{"$ref":"#/$defs/IconButton"},{"$ref":"#/$defs/Steps"},{"$ref":"#/$defs/InlineHeader"},{"$ref":"#/$defs/EntityList"},{"$ref":"#/$defs/EditableTable"},{"$ref":"#/$defs/SnippetCardBlock"},{"$ref":"#/$defs/OverviewCardBlock"},{"$ref":"#/$defs/ContextCardBlock"},{"$ref":"#/$defs/CompositeCardBlock"},{"$ref":"#/$defs/VisualCardBlock"}]}}},"required":["value","trigger","content"],"additionalProperties":false,"description":"value is unique id, trigger is section title"},"Modal":{"type":"object","properties":{"title":{"type":"string"},"open":{"type":"boolean"},"children":{"type":"array","items":{"anyOf":[{"$ref":"#/$defs/TextContent"},{"$ref":"#/$defs/MarkDownRenderer"},{"$ref":"#/$defs/CardHeader"},{"$ref":"#/$defs/Callout"},{"$ref":"#/$defs/TextCallout"},{"$ref":"#/$defs/CodeBlock"},{"$ref":"#/$defs/Image"},{"$ref":"#/$defs/ImageBlock"},{"$ref":"#/$defs/ImageGallery"},{"$ref":"#/$defs/Separator"},{"$ref":"#/$defs/HorizontalBarChart"},{"$ref":"#/$defs/RadarChart"},{"$ref":"#/$defs/PieChart"},{"$ref":"#/$defs/RadialChart"},{"$ref":"#/$defs/SingleStackedBarChart"},{"$ref":"#/$defs/ScatterChart"},{"$ref":"#/$defs/AreaChart"},{"$ref":"#/$defs/BarChart"},{"$ref":"#/$defs/LineChart"},{"$ref":"#/$defs/Table"},{"$ref":"#/$defs/TagBlock"},{"$ref":"#/$defs/Form"},{"$ref":"#/$defs/Buttons"},{"$ref":"#/$defs/IconButton"},{"$ref":"#/$defs/Steps"},{"$ref":"#/$defs/InlineHeader"},{"$ref":"#/$defs/EntityList"},{"$ref":"#/$defs/EditableTable"},{"$ref":"#/$defs/SnippetCardBlock"},{"$ref":"#/$defs/OverviewCardBlock"},{"$ref":"#/$defs/ContextCardBlock"},{"$ref":"#/$defs/CompositeCardBlock"},{"$ref":"#/$defs/VisualCardBlock"}]}},"size":{"type":"string","enum":["sm","md","lg"]}},"required":["title","children"],"additionalProperties":false,"description":"Modal dialog. open is a reactive $boolean binding — set to true to open, X/Escape/backdrop auto-closes. Put Form with buttons inside children."}}},"root":"Stack","reactive":{"Callout":["visible"],"Input":["value"],"TextArea":["value"],"Select":["value"],"DatePicker":["value"],"Slider":["value"],"CheckBoxGroup":["value"],"RadioGroup":["value"],"SwitchGroup":["value"],"Modal":["open"]}}, +"test": {"schema":{"$defs":{"Card":{"properties":{"children":{"type":"array","items":{"anyOf":[{"$ref":"#/$defs/Text"},{"$ref":"#/$defs/Button"},{"$ref":"#/$defs/Chart"}]}},"title":{"type":"string"}},"required":["children"]},"Text":{"properties":{"text":{"type":"string"},"size":{"type":"string","enum":["s","m","l"],"default":"m"}},"required":["text"]},"Button":{"properties":{"label":{"type":"string"},"action":{},"variant":{"type":"string","enum":["primary","secondary"]}},"required":["label"]},"Input":{"properties":{"name":{"type":"string"},"value":{"type":"string"}},"required":["name"]},"Chart":{"properties":{"labels":{"type":"array","items":{"type":"string"}},"values":{"type":"array","items":{"type":"number"}},"meta":{"type":"object","properties":{"unit":{"type":"string"},"scale":{"type":"number","default":1}},"required":["unit"]}},"required":["labels","values"]},"Tagged":{"properties":{"count":{"type":"integer"},"flag":{"type":"boolean"},"kind":{"const":"x"},"tone":{"type":"string","default":"neutral"}},"required":["tone"]}}},"root":"Card","reactive":{"Input":["value"]}}, +"hoisted": {"schema":{"properties":{"Tree":{}},"$defs":{"Tree":{"properties":{"node":{"$ref":"#/$defs/__schema0"}},"required":["node"]},"__schema0":{"type":"object","properties":{"label":{"type":"string"}}}}},"root":"Tree","reactive":{}} +} diff --git a/packages/swift-lang/Tests/OpenUILangTests/Fixtures/stream-set.json b/packages/swift-lang/Tests/OpenUILangTests/Fixtures/stream-set.json new file mode 100644 index 0000000000..d938569352 --- /dev/null +++ b/packages/swift-lang/Tests/OpenUILangTests/Fixtures/stream-set.json @@ -0,0 +1,8 @@ +[ +{"name":"basic","schema":"test","steps":[{"text":"root = Card([t1, b1])\nt1 = ","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["t1","b1"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"text":"root = Card([t1, b1])\nt1 = Text(\"Hi\")\nb1 = Button(\"Go\")","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"Hi"},"partial":false,"hasDynamicProps":false,"statementId":"t1"},{"type":"element","typeName":"Button","props":{"label":"Go"},"partial":false,"hasDynamicProps":false,"statementId":"b1"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":3,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"text":"root = Card([Text(\"replaced\")])","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"replaced"},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"missing-required","schema":"test","steps":[{"text":"root = Card([t1, b1])\nt1 ","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["t1","b1"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"text":"root = Card([t1, b1])\nt1 = Text()\nb1 = Button(\"Go\")","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Button","props":{"label":"Go"},"partial":false,"hasDynamicProps":false,"statementId":"b1"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":3,"errors":[{"code":"missing-required","component":"Text","path":"/text","message":"missing required field \"/text\" — signature: Text(text*: string, size: \"s\"|\"m\"|\"l\")","statementId":"t1"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"text":"root = Card([Text(\"replaced\")])","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"replaced"},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"null-required-with-default","schema":"test","steps":[{"text":"root = Card([t1]","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["t1"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"text":"root = Card([t1])\nt1 = Text(null)","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[{"code":"null-required","component":"Text","path":"/text","message":"required field \"/text\" cannot be null — signature: Text(text*: string, size: \"s\"|\"m\"|\"l\")","statementId":"t1"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"text":"root = Card([Text(\"replaced\")])","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"replaced"},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"required-default-fills","schema":"test","steps":[{"text":"root = Card([x])\nx = Ta","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["Ta"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"text":"root = Card([x])\nx = Tagged(1, true, \"x\", null)","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Tagged","props":{"count":1,"flag":true,"kind":"x","tone":"neutral"},"partial":false,"hasDynamicProps":false,"statementId":"x"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"text":"root = Card([Text(\"replaced\")])","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"replaced"},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"unknown-component","schema":"test","steps":[{"text":"root = Card([Myster","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["Myster"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"text":"root = Card([Mystery(\"a\"), Text(\"b\")])","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"b"},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"unknown-component","component":"Mystery","path":"","message":"Unknown component \"Mystery\" — not found in catalog or builtins. Available components: Card, Text, Button, Input, Chart, Tagged","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"text":"root = Card([Text(\"replaced\")])","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"replaced"},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"excess-args","schema":"test","steps":[{"text":"root = Card([Text(\"a","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"a"},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"text":"root = Card([Text(\"a\", \"m\", \"extra\", 4)])","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"a","size":"m"},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"excess-args","component":"Text","path":"","message":"Text takes 2 arg(s), got 4 (2 excess dropped)","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"text":"root = Card([Text(\"replaced\")])","expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"replaced"},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]} +] diff --git a/packages/swift-lang/Tests/OpenUILangTests/Fixtures/streaming.json b/packages/swift-lang/Tests/OpenUILangTests/Fixtures/streaming.json new file mode 100644 index 0000000000..3d562323e5 --- /dev/null +++ b/packages/swift-lang/Tests/OpenUILangTests/Fixtures/streaming.json @@ -0,0 +1,66 @@ +[ +{"name":"chat-example-1","schema":"chat","input":"Example 1 — Informational response (the shape for \"tell me about X\" / explainer asks):\n\nroot = Card([header, intro, highlights, statsHeader, statsCards, sectionsBlock, actionBtns], [src1, src2])\nheader = CardHeader(\"Paris\", \"The City of Light · France's Eternal Capital\")\nintro = TextContent(\"Paris is one of the world's most visited cities — a timeless blend of iconic landmarks, world-class cuisine, art, and romance [1]. In 2026 the city is buzzing with new energy: the **Grand Palais** has reopened after a €466M renovation, and you can now **swim in the Seine** for the first time in over a century [2].\")\nhighlights = ContextCardBlock([h1, h2, h3], \"grid\", true, { type: \"continue_conversation\", context: \"Tell me more about this aspect of Paris\" })\nh1 = ContextCardItem(\"art-museums\", \"Art & Museums\", \"143 museums including the Louvre & newly reopened Grand Palais.\", \"gray\", \"https://cdn.britannica.com/03/121003-050-2544BD4E/Interior-Louvre-Museum-Paris.jpg\")\nh2 = ContextCardItem(\"gastronomy\", \"Gastronomy\", \"9,000+ restaurants, 130+ Michelin stars, and the world's best baguettes.\", \"gray\")\nh3 = ContextCardItem(\"fashion-capital\", \"Fashion Capital\", \"Home to Chanel, Dior, Louis Vuitton & Hermès — the global luxury hub.\", \"gray\", \"https://thumbs.dreamstime.com/z/dior-storefront-facade-chanel-store-lvmh-s-french-designer-modehouse-christian-adjacent-to-luxury-fashion-beauty-brand-327161553.jpg\")\nstatsHeader = InlineHeader(\"Paris by the Numbers\", \"Key visitor statistics\")\nstatsCards = OverviewCardBlock([ov1, ov2, ov3], \"grid\", true)\nov1 = OverviewCardItem(\"annual-visitors\", ov1top, ov1metric)\nov1top = IconText(ov1icon, \"neutral\", \"m\", \"Annual Visitors\", \"2024 arrivals\", false, \"vertical\")\nov1icon = Icon(\"users\", \"people\")\nov1metric = MetricIndicatorInline(\"48.7M\", \"tourists\", { direction: \"up\", value: 2.5 })\nov2 = OverviewCardItem(\"museums-monuments\", ov2top, ov2metric)\nov2top = IconText(ov2icon, \"neutral\", \"m\", \"Museums & Monuments\", \"Cultural sites\", false, \"vertical\")\nov2icon = Icon(\"landmark\", \"buildings\")\nov2metric = MetricIndicatorInline(\"2,370+\", \"across the city\")\nov3 = OverviewCardItem(\"tourism-revenue\", ov3top, ov3metric)\nov3top = IconText(ov3icon, \"neutral\", \"m\", \"Tourism Revenue\", \"2024 estimate\", false, \"vertical\")\nov3icon = Icon(\"circle-dollar-sign\", \"finance\")\nov3metric = MetricIndicatorInline(\"€23.4B\", \"generated\")\nsectionsBlock = SectionBlock([secLandmarks, secFood, secWhen], false)\nsecLandmarks = SectionItem(\"landmarks\", \"Iconic Landmarks\", [landmarksCarousel])\nlandmarksCarousel = VisualCardBlock([lm1, lm2, lm3, lm4], \"carousel\", true, { type: \"continue_conversation\", context: \"Tell me more about this Paris landmark\" })\nlm1 = VisualCardItem(lm1body, \"eiffel-tower\", \"https://c8.alamy.com/comp/RYEB13/aerial-view-of-the-eiffel-tower-with-the-park-champ-de-mars-and-the-river-seine-paris-france-RYEB13.jpg\", lm1tag, \"Aerial view of the Eiffel Tower\")\nlm1body = BoldText(\"text\", \"Eiffel Tower\", \"Open daily 9am–midnight\")\nlm1tag = Tag(\"Must-See\", lm1tagIcon, \"sm\", \"info\")\nlm1tagIcon = Icon(\"star\", \"shapes\")\nlm2 = VisualCardItem(lm2body, \"the-louvre\", \"https://images.fineartamerica.com/images/artworkimages/mediumlarge/3/1-exterior-of-musee-du-louvre-museum-paris-france-bernard-jaubert.jpg\", lm2tag, \"Exterior of the Louvre museum\")\nlm2body = BoldText(\"text\", \"The Louvre\", \"World's largest art museum\")\nlm2tag = Tag(\"8.9M visits/yr\", lm2tagIcon, \"sm\", \"success\")\nlm2tagIcon = Icon(\"trending-up\", \"charts\")\nlm3 = VisualCardItem(lm3body, \"notre-dame\", \"https://cdn.britannica.com/29/255529-050-63A22A3C/notre-dame-de-paris-cathedral-paris-france.jpg\", lm3tag, \"Notre-Dame cathedral facade\")\nlm3body = BoldText(\"text\", \"Notre-Dame\", \"Fully restored cathedral\")\nlm3tag = Tag(\"Reopened 2024\", lm3tagIcon, \"sm\", \"warning\")\nlm3tagIcon = Icon(\"hammer\", \"tools\")\nlm4 = VisualCardItem(lm4body, \"versailles\", \"https://cdn.britannica.com/76/130076-050-31128A3D/Latona-Fountain-Balthazard-Marsy-Gaspard-Andre-Le.jpg\", lm4tag, \"Latona Fountain at Versailles\")\nlm4body = BoldText(\"text\", \"Palace of Versailles\", \"Royal gardens & grand halls\")\nlm4tag = Tag(\"Day Trip\", lm4tagIcon, \"sm\", \"neutral\")\nlm4tagIcon = Icon(\"train-front\", \"travel\")\nsecFood = SectionItem(\"food\", \"Food & Drink\", [foodCards])\nfoodCards = ContextCardBlock([f1, f2, f3], \"grid\", true, { type: \"continue_conversation\", context: \"Tell me more about eating and drinking in Paris\" })\nf1 = ContextCardItem(\"bistros\", \"Bistros\", \"Classic French fare — try Les Arlots for confit beef cheeks.\", \"gray\", \"https://everydayparisian.com/wp-content/uploads/2023/01/IMG_0221-768x1024.webp\")\nf2 = ContextCardItem(\"fine-dining\", \"Fine Dining\", \"Septime leads a new wave of creative Michelin-starred cuisine.\", \"gray\", \"https://production-data.worldofmouth.app/images/72b2e6db-97ea-49c4-aa5d-3b7c8121c812.jpg\")\nf3 = ContextCardItem(\"bakeries\", \"Bakeries\", \"Shinya Pain for sourdough. Grand Prix-winning baguettes citywide.\", \"gray\", \"https://www.davidlebovitz.com/wp-content/uploads/2018/09/Le-petit-grain-paris-bakery-boulangerie-patisserie-pastry-shop-8-640x895.jpg\")\nsecWhen = SectionItem(\"when\", \"Best Time to Visit\", [whenTabs])\nwhenTabs = Tabs([tabSpring, tabSummer])\ntabSpring = TabItem(\"spring\", \"Spring\", [springList])\nspringList = ListBlock([sp1, sp2, sp3], \"number\")\nsp1 = ListItem(\"Cherry blossoms at Parc de Sceaux\", \"March – May\", null, null, { type: \"continue_conversation\", context: \"Tell me about cherry blossom season in Paris\" })\nsp2 = ListItem(\"Picnics along the Seine & in city parks\", \"Warm, lively atmosphere\", null, null, { type: \"continue_conversation\", context: \"Where are the best picnic spots in Paris?\" })\nsp3 = ListItem(\"Fewer crowds than summer\", \"Ideal for sightseeing\", null, null, { type: \"continue_conversation\", context: \"How busy is Paris in spring?\" })\ntabSummer = TabItem(\"summer\", \"Summer\", [summerList])\nsummerList = ListBlock([su1, su2, su3], \"number\")\nsu1 = ListItem(\"Swim in the Seine — open July & August 2026\", \"First time in 102 years\", null, null, { type: \"continue_conversation\", context: \"Tell me about swimming in the Seine\" })\nsu2 = ListItem(\"Open-air cinema at Parc de la Villette\", \"Free films under the stars\", null, null, { type: \"continue_conversation\", context: \"Tell me about the open-air cinema at Parc de la Villette\" })\nsu3 = ListItem(\"Bastille Day celebrations on July 14\", \"Fireworks at the Eiffel Tower\", null, null, { type: \"continue_conversation\", context: \"What happens in Paris on Bastille Day?\" })\nactionBtns = Buttons([btn1, btn2, btn3], \"row\")\nbtn1 = Button(\"Plan a Trip to Paris\", { type: \"continue_conversation\", context: \"Help me plan a trip to Paris\" }, \"primary\")\nbtn2 = Button(\"Best Hotels in Paris\", { type: \"continue_conversation\", context: \"Show me the best hotels in Paris\" }, \"secondary\")\nbtn3 = Button(\"Build a Paris Itinerary\", { type: \"continue_conversation\", context: \"Build a 5-day Paris itinerary for me\" }, \"secondary\")\nsrc1 = { title: \"Paris Travel Guide 2025\", sourceName: \"Travel and Tour World\", url: \"https://www.travelandtourworld.com/news/article/from-iconic-landmarks-to-hidden-gems-the-ultimate-paris-travel-guide-for-2025-you-need-to-read-now/\" }\nsrc2 = { title: \"Best Things to Do in Paris 2026\", sourceName: \"Time Out Paris\", url: \"http://www.timeout.fr/paris/en/for-tourists\" }","checkpoints":[{"at":607,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Paris","subtitle":"The City of Light · France's Eternal Capital"},"partial":true,"hasDynamicProps":false,"statementId":"header"},{"type":"element","typeName":"TextContent","props":{"text":"Paris is one of the world's most visited cities — a timeless blend of iconic landmarks, world-class cuisine, art, and romance [1]. In 2026 the city is buzzing with new energy: the **Grand Palais** has reopened after a €466M renovation, and you can now **swim in the Seine** for the first time in over a century [2]"},"partial":true,"hasDynamicProps":false,"statementId":"intro"}],"sources":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["highlights","statsHeader","statsCards","sectionsBlock","actionBtns","src1","src2"],"orphaned":[],"statementCount":3,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":1214,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Paris","subtitle":"The City of Light · France's Eternal Capital"},"partial":true,"hasDynamicProps":false,"statementId":"header"},{"type":"element","typeName":"TextContent","props":{"text":"Paris is one of the world's most visited cities — a timeless blend of iconic landmarks, world-class cuisine, art, and romance [1]. In 2026 the city is buzzing with new energy: the **Grand Palais** has reopened after a €466M renovation, and you can now **swim in the Seine** for the first time in over a century [2]."},"partial":true,"hasDynamicProps":false,"statementId":"intro"},{"type":"element","typeName":"ContextCardBlock","props":{"items":[{"type":"element","typeName":"ContextCardItem","props":{"id":"art-museums","title":"Art & Museums","body":"143 museums including the Louvre & newly reopened Grand Palais.","bgColor":"gray","bgImageSrc":"https://cdn.britannica.com/03/121003-050-2544BD4E/Interior-Louvre-Museum-Paris.jpg"},"partial":true,"hasDynamicProps":false,"statementId":"h1"},{"type":"element","typeName":"ContextCardItem","props":{"id":"gastronomy","title":"Gastronomy","body":"9,000+ restaurants, 130+ Michelin stars, and the world's best baguettes.","bgColor":"gray"},"partial":true,"hasDynamicProps":false,"statementId":"h2"},{"type":"element","typeName":"ContextCardItem","props":{"id":"fashion-capital","title":"Fashion Capital","body":"Home to Chanel, Dior, Louis Vuitton & Hermès — the"},"partial":true,"hasDynamicProps":false,"statementId":"h3"}],"layout":"grid","responsive":true,"action":{"type":"continue_conversation","context":"Tell me more about this aspect of Paris"}},"partial":true,"hasDynamicProps":false,"statementId":"highlights"}],"sources":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["statsHeader","statsCards","sectionsBlock","actionBtns","src1","src2"],"orphaned":[],"statementCount":7,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":1821,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Paris","subtitle":"The City of Light · France's Eternal Capital"},"partial":true,"hasDynamicProps":false,"statementId":"header"},{"type":"element","typeName":"TextContent","props":{"text":"Paris is one of the world's most visited cities — a timeless blend of iconic landmarks, world-class cuisine, art, and romance [1]. In 2026 the city is buzzing with new energy: the **Grand Palais** has reopened after a €466M renovation, and you can now **swim in the Seine** for the first time in over a century [2]."},"partial":true,"hasDynamicProps":false,"statementId":"intro"},{"type":"element","typeName":"ContextCardBlock","props":{"items":[{"type":"element","typeName":"ContextCardItem","props":{"id":"art-museums","title":"Art & Museums","body":"143 museums including the Louvre & newly reopened Grand Palais.","bgColor":"gray","bgImageSrc":"https://cdn.britannica.com/03/121003-050-2544BD4E/Interior-Louvre-Museum-Paris.jpg"},"partial":true,"hasDynamicProps":false,"statementId":"h1"},{"type":"element","typeName":"ContextCardItem","props":{"id":"gastronomy","title":"Gastronomy","body":"9,000+ restaurants, 130+ Michelin stars, and the world's best baguettes.","bgColor":"gray"},"partial":true,"hasDynamicProps":false,"statementId":"h2"},{"type":"element","typeName":"ContextCardItem","props":{"id":"fashion-capital","title":"Fashion Capital","body":"Home to Chanel, Dior, Louis Vuitton & Hermès — the global luxury hub.","bgColor":"gray","bgImageSrc":"https://thumbs.dreamstime.com/z/dior-storefront-facade-chanel-store-lvmh-s-french-designer-modehouse-christian-adjacent-to-luxury-fashion-beauty-brand-327161553.jpg"},"partial":true,"hasDynamicProps":false,"statementId":"h3"}],"layout":"grid","responsive":true,"action":{"type":"continue_conversation","context":"Tell me more about this aspect of Paris"}},"partial":true,"hasDynamicProps":false,"statementId":"highlights"},{"type":"element","typeName":"InlineHeader","props":{"heading":"Paris by the Numbers","description":"Key visitor statistics"},"partial":true,"hasDynamicProps":false,"statementId":"statsHeader"},{"type":"element","typeName":"OverviewCardBlock","props":{"items":[{"type":"element","typeName":"OverviewCardItem","props":{"id":"annual-visitors","top":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"users","category":"people"},"partial":true,"hasDynamicProps":false,"statementId":"ov1icon"},"iconVariant":"neutral","iconSize":"m","title":"Annual Visitors","subtitle":"2024 arrivals","bold":false,"layout":"vertical"},"partial":true,"hasDynamicProps":false,"statementId":"ov1top"},"bottom":{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"48.7M","subtext":"tourists","trend":{"direction":"up","valu":null,"?":null}},"partial":true,"hasDynamicProps":false,"statementId":"ov1metric"}},"partial":true,"hasDynamicProps":false,"statementId":"ov1"}],"layout":"grid","responsive":true},"partial":true,"hasDynamicProps":false,"statementId":"statsCards"}],"sources":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["ov2","ov3","sectionsBlock","actionBtns","src1","src2"],"orphaned":[],"statementCount":13,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":2428,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Paris","subtitle":"The City of Light · France's Eternal Capital"},"partial":true,"hasDynamicProps":false,"statementId":"header"},{"type":"element","typeName":"TextContent","props":{"text":"Paris is one of the world's most visited cities — a timeless blend of iconic landmarks, world-class cuisine, art, and romance [1]. In 2026 the city is buzzing with new energy: the **Grand Palais** has reopened after a €466M renovation, and you can now **swim in the Seine** for the first time in over a century [2]."},"partial":true,"hasDynamicProps":false,"statementId":"intro"},{"type":"element","typeName":"ContextCardBlock","props":{"items":[{"type":"element","typeName":"ContextCardItem","props":{"id":"art-museums","title":"Art & Museums","body":"143 museums including the Louvre & newly reopened Grand Palais.","bgColor":"gray","bgImageSrc":"https://cdn.britannica.com/03/121003-050-2544BD4E/Interior-Louvre-Museum-Paris.jpg"},"partial":true,"hasDynamicProps":false,"statementId":"h1"},{"type":"element","typeName":"ContextCardItem","props":{"id":"gastronomy","title":"Gastronomy","body":"9,000+ restaurants, 130+ Michelin stars, and the world's best baguettes.","bgColor":"gray"},"partial":true,"hasDynamicProps":false,"statementId":"h2"},{"type":"element","typeName":"ContextCardItem","props":{"id":"fashion-capital","title":"Fashion Capital","body":"Home to Chanel, Dior, Louis Vuitton & Hermès — the global luxury hub.","bgColor":"gray","bgImageSrc":"https://thumbs.dreamstime.com/z/dior-storefront-facade-chanel-store-lvmh-s-french-designer-modehouse-christian-adjacent-to-luxury-fashion-beauty-brand-327161553.jpg"},"partial":true,"hasDynamicProps":false,"statementId":"h3"}],"layout":"grid","responsive":true,"action":{"type":"continue_conversation","context":"Tell me more about this aspect of Paris"}},"partial":true,"hasDynamicProps":false,"statementId":"highlights"},{"type":"element","typeName":"InlineHeader","props":{"heading":"Paris by the Numbers","description":"Key visitor statistics"},"partial":true,"hasDynamicProps":false,"statementId":"statsHeader"},{"type":"element","typeName":"OverviewCardBlock","props":{"items":[{"type":"element","typeName":"OverviewCardItem","props":{"id":"annual-visitors","top":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"users","category":"people"},"partial":true,"hasDynamicProps":false,"statementId":"ov1icon"},"iconVariant":"neutral","iconSize":"m","title":"Annual Visitors","subtitle":"2024 arrivals","bold":false,"layout":"vertical"},"partial":true,"hasDynamicProps":false,"statementId":"ov1top"},"bottom":{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"48.7M","subtext":"tourists","trend":{"direction":"up","value":2.5}},"partial":true,"hasDynamicProps":false,"statementId":"ov1metric"}},"partial":true,"hasDynamicProps":false,"statementId":"ov1"},{"type":"element","typeName":"OverviewCardItem","props":{"id":"museums-monuments","top":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"landmark","category":"buildings"},"partial":true,"hasDynamicProps":false,"statementId":"ov2icon"},"iconVariant":"neutral","iconSize":"m","title":"Museums & Monuments","subtitle":"Cultural sites","bold":false,"layout":"vertical"},"partial":true,"hasDynamicProps":false,"statementId":"ov2top"},"bottom":{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"2,370+","subtext":"across the city"},"partial":true,"hasDynamicProps":false,"statementId":"ov2metric"}},"partial":true,"hasDynamicProps":false,"statementId":"ov2"},{"type":"element","typeName":"OverviewCardItem","props":{"id":"tourism-revenue","top":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"circle-dollar-sign","category":"finance"},"partial":true,"hasDynamicProps":false,"statementId":"ov3icon"},"iconVariant":"neutral","iconSize":"m","title":"Tourism Revenue","subtitle":"2024 estimate","bold":false,"layout":"vertical"},"partial":true,"hasDynamicProps":false,"statementId":"ov3top"},"bottom":{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"€23.4B","subtext":"generated"},"partial":true,"hasDynamicProps":false,"statementId":"ov3metric"}},"partial":true,"hasDynamicProps":false,"statementId":"ov3"}],"layout":"grid","responsive":true},"partial":true,"hasDynamicProps":false,"statementId":"statsCards"},{"type":"element","typeName":"SectionBlock","props":{"sections":[],"isFoldable":null},"partial":true,"hasDynamicProps":false,"statementId":"sectionsBlock"}],"sources":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["secLandmarks","secFood","secWhen","f","actionBtns","src1","src2"],"orphaned":[],"statementCount":22,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":3035,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Paris","subtitle":"The City of Light · France's Eternal Capital"},"partial":false,"hasDynamicProps":false,"statementId":"header"},{"type":"element","typeName":"TextContent","props":{"text":"Paris is one of the world's most visited cities — a timeless blend of iconic landmarks, world-class cuisine, art, and romance [1]. In 2026 the city is buzzing with new energy: the **Grand Palais** has reopened after a €466M renovation, and you can now **swim in the Seine** for the first time in over a century [2]."},"partial":false,"hasDynamicProps":false,"statementId":"intro"},{"type":"element","typeName":"ContextCardBlock","props":{"items":[{"type":"element","typeName":"ContextCardItem","props":{"id":"art-museums","title":"Art & Museums","body":"143 museums including the Louvre & newly reopened Grand Palais.","bgColor":"gray","bgImageSrc":"https://cdn.britannica.com/03/121003-050-2544BD4E/Interior-Louvre-Museum-Paris.jpg"},"partial":false,"hasDynamicProps":false,"statementId":"h1"},{"type":"element","typeName":"ContextCardItem","props":{"id":"gastronomy","title":"Gastronomy","body":"9,000+ restaurants, 130+ Michelin stars, and the world's best baguettes.","bgColor":"gray"},"partial":false,"hasDynamicProps":false,"statementId":"h2"},{"type":"element","typeName":"ContextCardItem","props":{"id":"fashion-capital","title":"Fashion Capital","body":"Home to Chanel, Dior, Louis Vuitton & Hermès — the global luxury hub.","bgColor":"gray","bgImageSrc":"https://thumbs.dreamstime.com/z/dior-storefront-facade-chanel-store-lvmh-s-french-designer-modehouse-christian-adjacent-to-luxury-fashion-beauty-brand-327161553.jpg"},"partial":false,"hasDynamicProps":false,"statementId":"h3"}],"layout":"grid","responsive":true,"action":{"type":"continue_conversation","context":"Tell me more about this aspect of Paris"}},"partial":false,"hasDynamicProps":false,"statementId":"highlights"},{"type":"element","typeName":"InlineHeader","props":{"heading":"Paris by the Numbers","description":"Key visitor statistics"},"partial":false,"hasDynamicProps":false,"statementId":"statsHeader"},{"type":"element","typeName":"OverviewCardBlock","props":{"items":[{"type":"element","typeName":"OverviewCardItem","props":{"id":"annual-visitors","top":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"users","category":"people"},"partial":false,"hasDynamicProps":false,"statementId":"ov1icon"},"iconVariant":"neutral","iconSize":"m","title":"Annual Visitors","subtitle":"2024 arrivals","bold":false,"layout":"vertical"},"partial":false,"hasDynamicProps":false,"statementId":"ov1top"},"bottom":{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"48.7M","subtext":"tourists","trend":{"direction":"up","value":2.5}},"partial":false,"hasDynamicProps":false,"statementId":"ov1metric"}},"partial":false,"hasDynamicProps":false,"statementId":"ov1"},{"type":"element","typeName":"OverviewCardItem","props":{"id":"museums-monuments","top":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"landmark","category":"buildings"},"partial":false,"hasDynamicProps":false,"statementId":"ov2icon"},"iconVariant":"neutral","iconSize":"m","title":"Museums & Monuments","subtitle":"Cultural sites","bold":false,"layout":"vertical"},"partial":false,"hasDynamicProps":false,"statementId":"ov2top"},"bottom":{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"2,370+","subtext":"across the city"},"partial":false,"hasDynamicProps":false,"statementId":"ov2metric"}},"partial":false,"hasDynamicProps":false,"statementId":"ov2"},{"type":"element","typeName":"OverviewCardItem","props":{"id":"tourism-revenue","top":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"circle-dollar-sign","category":"finance"},"partial":false,"hasDynamicProps":false,"statementId":"ov3icon"},"iconVariant":"neutral","iconSize":"m","title":"Tourism Revenue","subtitle":"2024 estimate","bold":false,"layout":"vertical"},"partial":false,"hasDynamicProps":false,"statementId":"ov3top"},"bottom":{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"€23.4B","subtext":"generated"},"partial":false,"hasDynamicProps":false,"statementId":"ov3metric"}},"partial":false,"hasDynamicProps":false,"statementId":"ov3"}],"layout":"grid","responsive":true},"partial":false,"hasDynamicProps":false,"statementId":"statsCards"},{"type":"element","typeName":"SectionBlock","props":{"sections":[{"type":"element","typeName":"SectionItem","props":{"value":"landmarks","trigger":"Iconic Landmarks","content":[{"type":"element","typeName":"VisualCardBlock","props":{"items":[{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Eiffel Tower","subtext":"Open daily 9am–midnight","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"lm1body"},"id":"eiffel-tower","bgImageSrc":"https://c8.alamy.com/comp/RYEB13/aerial-view-of-the-eiffel-tower-with-the-park-champ-de-mars-and-the-river-seine-paris-france-RYEB13.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Must-See","icon":null,"size":"sm","variant":"info"},"partial":false,"hasDynamicProps":false,"statementId":"lm1tag"},"bgImageAlt":"Aerial view of the Eiffel Tower"},"partial":false,"hasDynamicProps":false,"statementId":"lm1"}],"layout":"carousel","responsive":true,"action":{"type":"continue_conversation","context":"Tell me more about this Paris landmark"}},"partial":false,"hasDynamicProps":false,"statementId":"landmarksCarousel"}]},"partial":false,"hasDynamicProps":false,"statementId":"secLandmarks"}],"isFoldable":false},"partial":false,"hasDynamicProps":false,"statementId":"sectionsBlock"}],"sources":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["lm1tagIcon","lm2","lm3","lm4","secFood","secWhen","actionBtns","src1","src2"],"orphaned":[],"statementCount":27,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":3642,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Paris","subtitle":"The City of Light · France's Eternal Capital"},"partial":true,"hasDynamicProps":false,"statementId":"header"},{"type":"element","typeName":"TextContent","props":{"text":"Paris is one of the world's most visited cities — a timeless blend of iconic landmarks, world-class cuisine, art, and romance [1]. In 2026 the city is buzzing with new energy: the **Grand Palais** has reopened after a €466M renovation, and you can now **swim in the Seine** for the first time in over a century [2]."},"partial":true,"hasDynamicProps":false,"statementId":"intro"},{"type":"element","typeName":"ContextCardBlock","props":{"items":[{"type":"element","typeName":"ContextCardItem","props":{"id":"art-museums","title":"Art & Museums","body":"143 museums including the Louvre & newly reopened Grand Palais.","bgColor":"gray","bgImageSrc":"https://cdn.britannica.com/03/121003-050-2544BD4E/Interior-Louvre-Museum-Paris.jpg"},"partial":true,"hasDynamicProps":false,"statementId":"h1"},{"type":"element","typeName":"ContextCardItem","props":{"id":"gastronomy","title":"Gastronomy","body":"9,000+ restaurants, 130+ Michelin stars, and the world's best baguettes.","bgColor":"gray"},"partial":true,"hasDynamicProps":false,"statementId":"h2"},{"type":"element","typeName":"ContextCardItem","props":{"id":"fashion-capital","title":"Fashion Capital","body":"Home to Chanel, Dior, Louis Vuitton & Hermès — the global luxury hub.","bgColor":"gray","bgImageSrc":"https://thumbs.dreamstime.com/z/dior-storefront-facade-chanel-store-lvmh-s-french-designer-modehouse-christian-adjacent-to-luxury-fashion-beauty-brand-327161553.jpg"},"partial":true,"hasDynamicProps":false,"statementId":"h3"}],"layout":"grid","responsive":true,"action":{"type":"continue_conversation","context":"Tell me more about this aspect of Paris"}},"partial":true,"hasDynamicProps":false,"statementId":"highlights"},{"type":"element","typeName":"InlineHeader","props":{"heading":"Paris by the Numbers","description":"Key visitor statistics"},"partial":true,"hasDynamicProps":false,"statementId":"statsHeader"},{"type":"element","typeName":"OverviewCardBlock","props":{"items":[{"type":"element","typeName":"OverviewCardItem","props":{"id":"annual-visitors","top":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"users","category":"people"},"partial":true,"hasDynamicProps":false,"statementId":"ov1icon"},"iconVariant":"neutral","iconSize":"m","title":"Annual Visitors","subtitle":"2024 arrivals","bold":false,"layout":"vertical"},"partial":true,"hasDynamicProps":false,"statementId":"ov1top"},"bottom":{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"48.7M","subtext":"tourists","trend":{"direction":"up","value":2.5}},"partial":true,"hasDynamicProps":false,"statementId":"ov1metric"}},"partial":true,"hasDynamicProps":false,"statementId":"ov1"},{"type":"element","typeName":"OverviewCardItem","props":{"id":"museums-monuments","top":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"landmark","category":"buildings"},"partial":true,"hasDynamicProps":false,"statementId":"ov2icon"},"iconVariant":"neutral","iconSize":"m","title":"Museums & Monuments","subtitle":"Cultural sites","bold":false,"layout":"vertical"},"partial":true,"hasDynamicProps":false,"statementId":"ov2top"},"bottom":{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"2,370+","subtext":"across the city"},"partial":true,"hasDynamicProps":false,"statementId":"ov2metric"}},"partial":true,"hasDynamicProps":false,"statementId":"ov2"},{"type":"element","typeName":"OverviewCardItem","props":{"id":"tourism-revenue","top":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"circle-dollar-sign","category":"finance"},"partial":true,"hasDynamicProps":false,"statementId":"ov3icon"},"iconVariant":"neutral","iconSize":"m","title":"Tourism Revenue","subtitle":"2024 estimate","bold":false,"layout":"vertical"},"partial":true,"hasDynamicProps":false,"statementId":"ov3top"},"bottom":{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"€23.4B","subtext":"generated"},"partial":true,"hasDynamicProps":false,"statementId":"ov3metric"}},"partial":true,"hasDynamicProps":false,"statementId":"ov3"}],"layout":"grid","responsive":true},"partial":true,"hasDynamicProps":false,"statementId":"statsCards"},{"type":"element","typeName":"SectionBlock","props":{"sections":[{"type":"element","typeName":"SectionItem","props":{"value":"landmarks","trigger":"Iconic Landmarks","content":[{"type":"element","typeName":"VisualCardBlock","props":{"items":[{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Eiffel Tower","subtext":"Open daily 9am–midnight","subtextVariant":"text","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"lm1body"},"id":"eiffel-tower","bgImageSrc":"https://c8.alamy.com/comp/RYEB13/aerial-view-of-the-eiffel-tower-with-the-park-champ-de-mars-and-the-river-seine-paris-france-RYEB13.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Must-See","icon":{"type":"element","typeName":"Icon","props":{"name":"star","category":"shapes"},"partial":true,"hasDynamicProps":false,"statementId":"lm1tagIcon"},"size":"sm","variant":"info"},"partial":true,"hasDynamicProps":false,"statementId":"lm1tag"},"bgImageAlt":"Aerial view of the Eiffel Tower"},"partial":true,"hasDynamicProps":false,"statementId":"lm1"},{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"The Louvre","subtext":"World's largest art museum","subtextVariant":"text","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"lm2body"},"id":"the-louvre","bgImageSrc":"https://images.fineartamerica.com/images/artworkimages/mediumlarge/3/1-exterior-of-musee-du-louvre-museum-paris-france-bernard-jaubert.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"8.9M visits/yr","icon":{"type":"element","typeName":"Icon","props":{"name":"trending-up","category":"charts"},"partial":true,"hasDynamicProps":false,"statementId":"lm2tagIcon"},"size":"sm","variant":"success"},"partial":true,"hasDynamicProps":false,"statementId":"lm2tag"},"bgImageAlt":"Exterior of the Louvre museum"},"partial":true,"hasDynamicProps":false,"statementId":"lm2"}],"layout":"carousel","responsive":true,"action":{"type":"continue_conversation","context":"Tell me more about this Paris landmark"}},"partial":true,"hasDynamicProps":false,"statementId":"landmarksCarousel"}]},"partial":true,"hasDynamicProps":false,"statementId":"secLandmarks"}],"isFoldable":false},"partial":true,"hasDynamicProps":false,"statementId":"sectionsBlock"}],"sources":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["lm3body","lm3tag","lm4","secFood","secWhen","actionBtns","src1","src2"],"orphaned":[],"statementCount":33,"errors":[{"code":"null-required","component":"VisualCardItem","path":"/body","message":"required field \"/body\" cannot be null — signature: VisualCardItem(body*: BoldText, id: string, bgImageSrc: string, tag: Tag, bgImageAlt: string)","statementId":"lm3"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":4249,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Paris","subtitle":"The City of Light · France's Eternal Capital"},"partial":false,"hasDynamicProps":false,"statementId":"header"},{"type":"element","typeName":"TextContent","props":{"text":"Paris is one of the world's most visited cities — a timeless blend of iconic landmarks, world-class cuisine, art, and romance [1]. In 2026 the city is buzzing with new energy: the **Grand Palais** has reopened after a €466M renovation, and you can now **swim in the Seine** for the first time in over a century [2]."},"partial":false,"hasDynamicProps":false,"statementId":"intro"},{"type":"element","typeName":"ContextCardBlock","props":{"items":[{"type":"element","typeName":"ContextCardItem","props":{"id":"art-museums","title":"Art & Museums","body":"143 museums including the Louvre & newly reopened Grand Palais.","bgColor":"gray","bgImageSrc":"https://cdn.britannica.com/03/121003-050-2544BD4E/Interior-Louvre-Museum-Paris.jpg"},"partial":false,"hasDynamicProps":false,"statementId":"h1"},{"type":"element","typeName":"ContextCardItem","props":{"id":"gastronomy","title":"Gastronomy","body":"9,000+ restaurants, 130+ Michelin stars, and the world's best baguettes.","bgColor":"gray"},"partial":false,"hasDynamicProps":false,"statementId":"h2"},{"type":"element","typeName":"ContextCardItem","props":{"id":"fashion-capital","title":"Fashion Capital","body":"Home to Chanel, Dior, Louis Vuitton & Hermès — the global luxury hub.","bgColor":"gray","bgImageSrc":"https://thumbs.dreamstime.com/z/dior-storefront-facade-chanel-store-lvmh-s-french-designer-modehouse-christian-adjacent-to-luxury-fashion-beauty-brand-327161553.jpg"},"partial":false,"hasDynamicProps":false,"statementId":"h3"}],"layout":"grid","responsive":true,"action":{"type":"continue_conversation","context":"Tell me more about this aspect of Paris"}},"partial":false,"hasDynamicProps":false,"statementId":"highlights"},{"type":"element","typeName":"InlineHeader","props":{"heading":"Paris by the Numbers","description":"Key visitor statistics"},"partial":false,"hasDynamicProps":false,"statementId":"statsHeader"},{"type":"element","typeName":"OverviewCardBlock","props":{"items":[{"type":"element","typeName":"OverviewCardItem","props":{"id":"annual-visitors","top":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"users","category":"people"},"partial":false,"hasDynamicProps":false,"statementId":"ov1icon"},"iconVariant":"neutral","iconSize":"m","title":"Annual Visitors","subtitle":"2024 arrivals","bold":false,"layout":"vertical"},"partial":false,"hasDynamicProps":false,"statementId":"ov1top"},"bottom":{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"48.7M","subtext":"tourists","trend":{"direction":"up","value":2.5}},"partial":false,"hasDynamicProps":false,"statementId":"ov1metric"}},"partial":false,"hasDynamicProps":false,"statementId":"ov1"},{"type":"element","typeName":"OverviewCardItem","props":{"id":"museums-monuments","top":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"landmark","category":"buildings"},"partial":false,"hasDynamicProps":false,"statementId":"ov2icon"},"iconVariant":"neutral","iconSize":"m","title":"Museums & Monuments","subtitle":"Cultural sites","bold":false,"layout":"vertical"},"partial":false,"hasDynamicProps":false,"statementId":"ov2top"},"bottom":{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"2,370+","subtext":"across the city"},"partial":false,"hasDynamicProps":false,"statementId":"ov2metric"}},"partial":false,"hasDynamicProps":false,"statementId":"ov2"},{"type":"element","typeName":"OverviewCardItem","props":{"id":"tourism-revenue","top":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"circle-dollar-sign","category":"finance"},"partial":false,"hasDynamicProps":false,"statementId":"ov3icon"},"iconVariant":"neutral","iconSize":"m","title":"Tourism Revenue","subtitle":"2024 estimate","bold":false,"layout":"vertical"},"partial":false,"hasDynamicProps":false,"statementId":"ov3top"},"bottom":{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"€23.4B","subtext":"generated"},"partial":false,"hasDynamicProps":false,"statementId":"ov3metric"}},"partial":false,"hasDynamicProps":false,"statementId":"ov3"}],"layout":"grid","responsive":true},"partial":false,"hasDynamicProps":false,"statementId":"statsCards"},{"type":"element","typeName":"SectionBlock","props":{"sections":[{"type":"element","typeName":"SectionItem","props":{"value":"landmarks","trigger":"Iconic Landmarks","content":[{"type":"element","typeName":"VisualCardBlock","props":{"items":[{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Eiffel Tower","subtext":"Open daily 9am–midnight","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"lm1body"},"id":"eiffel-tower","bgImageSrc":"https://c8.alamy.com/comp/RYEB13/aerial-view-of-the-eiffel-tower-with-the-park-champ-de-mars-and-the-river-seine-paris-france-RYEB13.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Must-See","icon":{"type":"element","typeName":"Icon","props":{"name":"star","category":"shapes"},"partial":false,"hasDynamicProps":false,"statementId":"lm1tagIcon"},"size":"sm","variant":"info"},"partial":false,"hasDynamicProps":false,"statementId":"lm1tag"},"bgImageAlt":"Aerial view of the Eiffel Tower"},"partial":false,"hasDynamicProps":false,"statementId":"lm1"},{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"The Louvre","subtext":"World's largest art museum","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"lm2body"},"id":"the-louvre","bgImageSrc":"https://images.fineartamerica.com/images/artworkimages/mediumlarge/3/1-exterior-of-musee-du-louvre-museum-paris-france-bernard-jaubert.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"8.9M visits/yr","icon":{"type":"element","typeName":"Icon","props":{"name":"trending-up","category":"charts"},"partial":false,"hasDynamicProps":false,"statementId":"lm2tagIcon"},"size":"sm","variant":"success"},"partial":false,"hasDynamicProps":false,"statementId":"lm2tag"},"bgImageAlt":"Exterior of the Louvre museum"},"partial":false,"hasDynamicProps":false,"statementId":"lm2"},{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Notre-Dame","subtext":"Fully restored cathedral","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"lm3body"},"id":"notre-dame","bgImageSrc":"https://cdn.britannica.com/29/255529-050-63A22A3C/notre-dame-de-paris-cathedral-paris-france.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Reopened 2024","icon":{"type":"element","typeName":"Icon","props":{"name":"hammer","category":"tools"},"partial":false,"hasDynamicProps":false,"statementId":"lm3tagIcon"},"size":"sm","variant":"warning"},"partial":false,"hasDynamicProps":false,"statementId":"lm3tag"},"bgImageAlt":"Notre-Dame cathedral facade"},"partial":false,"hasDynamicProps":false,"statementId":"lm3"},{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Palace of Versailles","subtext":"Royal gardens & grand halls","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"lm4body"},"id":"versailles","bgImageSrc":"https://cdn.britannica.com/76/130076-050-31128A3D/Latona-Fountain-Balthazard-Marsy-Gaspard-Andre-Le.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Day Trip","icon":{"type":"element","typeName":"Icon","props":{"name":"train-front","category":"travel"},"partial":false,"hasDynamicProps":false,"statementId":"lm4tagIcon"},"size":"sm","variant":"neutral"},"partial":false,"hasDynamicProps":false,"statementId":"lm4tag"},"bgImageAlt":"Latona Fountain at Versailles"},"partial":false,"hasDynamicProps":false,"statementId":"lm4"}],"layout":"carousel","responsive":true,"action":{"type":"continue_conversation","context":"Tell me more about this Paris landmark"}},"partial":false,"hasDynamicProps":false,"statementId":"landmarksCarousel"}]},"partial":false,"hasDynamicProps":false,"statementId":"secLandmarks"},{"type":"element","typeName":"SectionItem","props":{"value":"food","trigger":"Food & Drink","content":[]},"partial":false,"hasDynamicProps":false,"statementId":"secFood"}],"isFoldable":false},"partial":false,"hasDynamicProps":false,"statementId":"sectionsBlock"}],"sources":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["foodCards","secWhen","actionBtns","src1","src2"],"orphaned":[],"statementCount":41,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":4856,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Paris","subtitle":"The City of Light · France's Eternal Capital"},"partial":true,"hasDynamicProps":false,"statementId":"header"},{"type":"element","typeName":"TextContent","props":{"text":"Paris is one of the world's most visited cities — a timeless blend of iconic landmarks, world-class cuisine, art, and romance [1]. In 2026 the city is buzzing with new energy: the **Grand Palais** has reopened after a €466M renovation, and you can now **swim in the Seine** for the first time in over a century [2]."},"partial":true,"hasDynamicProps":false,"statementId":"intro"},{"type":"element","typeName":"ContextCardBlock","props":{"items":[{"type":"element","typeName":"ContextCardItem","props":{"id":"art-museums","title":"Art & Museums","body":"143 museums including the Louvre & newly reopened Grand Palais.","bgColor":"gray","bgImageSrc":"https://cdn.britannica.com/03/121003-050-2544BD4E/Interior-Louvre-Museum-Paris.jpg"},"partial":true,"hasDynamicProps":false,"statementId":"h1"},{"type":"element","typeName":"ContextCardItem","props":{"id":"gastronomy","title":"Gastronomy","body":"9,000+ restaurants, 130+ Michelin stars, and the world's best baguettes.","bgColor":"gray"},"partial":true,"hasDynamicProps":false,"statementId":"h2"},{"type":"element","typeName":"ContextCardItem","props":{"id":"fashion-capital","title":"Fashion Capital","body":"Home to Chanel, Dior, Louis Vuitton & Hermès — the global luxury hub.","bgColor":"gray","bgImageSrc":"https://thumbs.dreamstime.com/z/dior-storefront-facade-chanel-store-lvmh-s-french-designer-modehouse-christian-adjacent-to-luxury-fashion-beauty-brand-327161553.jpg"},"partial":true,"hasDynamicProps":false,"statementId":"h3"}],"layout":"grid","responsive":true,"action":{"type":"continue_conversation","context":"Tell me more about this aspect of Paris"}},"partial":true,"hasDynamicProps":false,"statementId":"highlights"},{"type":"element","typeName":"InlineHeader","props":{"heading":"Paris by the Numbers","description":"Key visitor statistics"},"partial":true,"hasDynamicProps":false,"statementId":"statsHeader"},{"type":"element","typeName":"OverviewCardBlock","props":{"items":[{"type":"element","typeName":"OverviewCardItem","props":{"id":"annual-visitors","top":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"users","category":"people"},"partial":true,"hasDynamicProps":false,"statementId":"ov1icon"},"iconVariant":"neutral","iconSize":"m","title":"Annual Visitors","subtitle":"2024 arrivals","bold":false,"layout":"vertical"},"partial":true,"hasDynamicProps":false,"statementId":"ov1top"},"bottom":{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"48.7M","subtext":"tourists","trend":{"direction":"up","value":2.5}},"partial":true,"hasDynamicProps":false,"statementId":"ov1metric"}},"partial":true,"hasDynamicProps":false,"statementId":"ov1"},{"type":"element","typeName":"OverviewCardItem","props":{"id":"museums-monuments","top":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"landmark","category":"buildings"},"partial":true,"hasDynamicProps":false,"statementId":"ov2icon"},"iconVariant":"neutral","iconSize":"m","title":"Museums & Monuments","subtitle":"Cultural sites","bold":false,"layout":"vertical"},"partial":true,"hasDynamicProps":false,"statementId":"ov2top"},"bottom":{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"2,370+","subtext":"across the city"},"partial":true,"hasDynamicProps":false,"statementId":"ov2metric"}},"partial":true,"hasDynamicProps":false,"statementId":"ov2"},{"type":"element","typeName":"OverviewCardItem","props":{"id":"tourism-revenue","top":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"circle-dollar-sign","category":"finance"},"partial":true,"hasDynamicProps":false,"statementId":"ov3icon"},"iconVariant":"neutral","iconSize":"m","title":"Tourism Revenue","subtitle":"2024 estimate","bold":false,"layout":"vertical"},"partial":true,"hasDynamicProps":false,"statementId":"ov3top"},"bottom":{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"€23.4B","subtext":"generated"},"partial":true,"hasDynamicProps":false,"statementId":"ov3metric"}},"partial":true,"hasDynamicProps":false,"statementId":"ov3"}],"layout":"grid","responsive":true},"partial":true,"hasDynamicProps":false,"statementId":"statsCards"},{"type":"element","typeName":"SectionBlock","props":{"sections":[{"type":"element","typeName":"SectionItem","props":{"value":"landmarks","trigger":"Iconic Landmarks","content":[{"type":"element","typeName":"VisualCardBlock","props":{"items":[{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Eiffel Tower","subtext":"Open daily 9am–midnight","subtextVariant":"text","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"lm1body"},"id":"eiffel-tower","bgImageSrc":"https://c8.alamy.com/comp/RYEB13/aerial-view-of-the-eiffel-tower-with-the-park-champ-de-mars-and-the-river-seine-paris-france-RYEB13.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Must-See","icon":{"type":"element","typeName":"Icon","props":{"name":"star","category":"shapes"},"partial":true,"hasDynamicProps":false,"statementId":"lm1tagIcon"},"size":"sm","variant":"info"},"partial":true,"hasDynamicProps":false,"statementId":"lm1tag"},"bgImageAlt":"Aerial view of the Eiffel Tower"},"partial":true,"hasDynamicProps":false,"statementId":"lm1"},{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"The Louvre","subtext":"World's largest art museum","subtextVariant":"text","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"lm2body"},"id":"the-louvre","bgImageSrc":"https://images.fineartamerica.com/images/artworkimages/mediumlarge/3/1-exterior-of-musee-du-louvre-museum-paris-france-bernard-jaubert.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"8.9M visits/yr","icon":{"type":"element","typeName":"Icon","props":{"name":"trending-up","category":"charts"},"partial":true,"hasDynamicProps":false,"statementId":"lm2tagIcon"},"size":"sm","variant":"success"},"partial":true,"hasDynamicProps":false,"statementId":"lm2tag"},"bgImageAlt":"Exterior of the Louvre museum"},"partial":true,"hasDynamicProps":false,"statementId":"lm2"},{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Notre-Dame","subtext":"Fully restored cathedral","subtextVariant":"text","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"lm3body"},"id":"notre-dame","bgImageSrc":"https://cdn.britannica.com/29/255529-050-63A22A3C/notre-dame-de-paris-cathedral-paris-france.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Reopened 2024","icon":{"type":"element","typeName":"Icon","props":{"name":"hammer","category":"tools"},"partial":true,"hasDynamicProps":false,"statementId":"lm3tagIcon"},"size":"sm","variant":"warning"},"partial":true,"hasDynamicProps":false,"statementId":"lm3tag"},"bgImageAlt":"Notre-Dame cathedral facade"},"partial":true,"hasDynamicProps":false,"statementId":"lm3"},{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Palace of Versailles","subtext":"Royal gardens & grand halls","subtextVariant":"text","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"lm4body"},"id":"versailles","bgImageSrc":"https://cdn.britannica.com/76/130076-050-31128A3D/Latona-Fountain-Balthazard-Marsy-Gaspard-Andre-Le.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Day Trip","icon":{"type":"element","typeName":"Icon","props":{"name":"train-front","category":"travel"},"partial":true,"hasDynamicProps":false,"statementId":"lm4tagIcon"},"size":"sm","variant":"neutral"},"partial":true,"hasDynamicProps":false,"statementId":"lm4tag"},"bgImageAlt":"Latona Fountain at Versailles"},"partial":true,"hasDynamicProps":false,"statementId":"lm4"}],"layout":"carousel","responsive":true,"action":{"type":"continue_conversation","context":"Tell me more about this Paris landmark"}},"partial":true,"hasDynamicProps":false,"statementId":"landmarksCarousel"}]},"partial":true,"hasDynamicProps":false,"statementId":"secLandmarks"},{"type":"element","typeName":"SectionItem","props":{"value":"food","trigger":"Food & Drink","content":[{"type":"element","typeName":"ContextCardBlock","props":{"items":[{"type":"element","typeName":"ContextCardItem","props":{"id":"bistros","title":"Bistros","body":"Classic French fare — try Les Arlots for confit beef cheeks.","bgColor":"gray","bgImageSrc":"https://everydayparisian.com/wp-content/uploads/2023/01/IMG_0221-768x1024.webp"},"partial":true,"hasDynamicProps":false,"statementId":"f1"},{"type":"element","typeName":"ContextCardItem","props":{"id":"fine-dining","title":"Fine Dining","body":"Septime leads a new wave of creative Michelin-starred cuisine.","bgColor":"gray","bgImageSrc":"https://production-data.worldofmouth.app/images/72b2e6db-97ea-49c4-aa5d-3b7c8121c812.jpg"},"partial":true,"hasDynamicProps":false,"statementId":"f2"},{"type":"element","typeName":"ContextCardItem","props":{"id":"bakeries","title":"Bakeries","body":"Sh"},"partial":true,"hasDynamicProps":false,"statementId":"f3"}],"layout":"grid","responsive":true,"action":{"type":"continue_conversation","context":"Tell me more about eating and drinking in Paris"}},"partial":true,"hasDynamicProps":false,"statementId":"foodCards"}]},"partial":true,"hasDynamicProps":false,"statementId":"secFood"}],"isFoldable":false},"partial":true,"hasDynamicProps":false,"statementId":"sectionsBlock"}],"sources":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["secWhen","actionBtns","src1","src2"],"orphaned":[],"statementCount":45,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":5463,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Paris","subtitle":"The City of Light · France's Eternal Capital"},"partial":true,"hasDynamicProps":false,"statementId":"header"},{"type":"element","typeName":"TextContent","props":{"text":"Paris is one of the world's most visited cities — a timeless blend of iconic landmarks, world-class cuisine, art, and romance [1]. In 2026 the city is buzzing with new energy: the **Grand Palais** has reopened after a €466M renovation, and you can now **swim in the Seine** for the first time in over a century [2]."},"partial":true,"hasDynamicProps":false,"statementId":"intro"},{"type":"element","typeName":"ContextCardBlock","props":{"items":[{"type":"element","typeName":"ContextCardItem","props":{"id":"art-museums","title":"Art & Museums","body":"143 museums including the Louvre & newly reopened Grand Palais.","bgColor":"gray","bgImageSrc":"https://cdn.britannica.com/03/121003-050-2544BD4E/Interior-Louvre-Museum-Paris.jpg"},"partial":true,"hasDynamicProps":false,"statementId":"h1"},{"type":"element","typeName":"ContextCardItem","props":{"id":"gastronomy","title":"Gastronomy","body":"9,000+ restaurants, 130+ Michelin stars, and the world's best baguettes.","bgColor":"gray"},"partial":true,"hasDynamicProps":false,"statementId":"h2"},{"type":"element","typeName":"ContextCardItem","props":{"id":"fashion-capital","title":"Fashion Capital","body":"Home to Chanel, Dior, Louis Vuitton & Hermès — the global luxury hub.","bgColor":"gray","bgImageSrc":"https://thumbs.dreamstime.com/z/dior-storefront-facade-chanel-store-lvmh-s-french-designer-modehouse-christian-adjacent-to-luxury-fashion-beauty-brand-327161553.jpg"},"partial":true,"hasDynamicProps":false,"statementId":"h3"}],"layout":"grid","responsive":true,"action":{"type":"continue_conversation","context":"Tell me more about this aspect of Paris"}},"partial":true,"hasDynamicProps":false,"statementId":"highlights"},{"type":"element","typeName":"InlineHeader","props":{"heading":"Paris by the Numbers","description":"Key visitor statistics"},"partial":true,"hasDynamicProps":false,"statementId":"statsHeader"},{"type":"element","typeName":"OverviewCardBlock","props":{"items":[{"type":"element","typeName":"OverviewCardItem","props":{"id":"annual-visitors","top":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"users","category":"people"},"partial":true,"hasDynamicProps":false,"statementId":"ov1icon"},"iconVariant":"neutral","iconSize":"m","title":"Annual Visitors","subtitle":"2024 arrivals","bold":false,"layout":"vertical"},"partial":true,"hasDynamicProps":false,"statementId":"ov1top"},"bottom":{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"48.7M","subtext":"tourists","trend":{"direction":"up","value":2.5}},"partial":true,"hasDynamicProps":false,"statementId":"ov1metric"}},"partial":true,"hasDynamicProps":false,"statementId":"ov1"},{"type":"element","typeName":"OverviewCardItem","props":{"id":"museums-monuments","top":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"landmark","category":"buildings"},"partial":true,"hasDynamicProps":false,"statementId":"ov2icon"},"iconVariant":"neutral","iconSize":"m","title":"Museums & Monuments","subtitle":"Cultural sites","bold":false,"layout":"vertical"},"partial":true,"hasDynamicProps":false,"statementId":"ov2top"},"bottom":{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"2,370+","subtext":"across the city"},"partial":true,"hasDynamicProps":false,"statementId":"ov2metric"}},"partial":true,"hasDynamicProps":false,"statementId":"ov2"},{"type":"element","typeName":"OverviewCardItem","props":{"id":"tourism-revenue","top":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"circle-dollar-sign","category":"finance"},"partial":true,"hasDynamicProps":false,"statementId":"ov3icon"},"iconVariant":"neutral","iconSize":"m","title":"Tourism Revenue","subtitle":"2024 estimate","bold":false,"layout":"vertical"},"partial":true,"hasDynamicProps":false,"statementId":"ov3top"},"bottom":{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"€23.4B","subtext":"generated"},"partial":true,"hasDynamicProps":false,"statementId":"ov3metric"}},"partial":true,"hasDynamicProps":false,"statementId":"ov3"}],"layout":"grid","responsive":true},"partial":true,"hasDynamicProps":false,"statementId":"statsCards"},{"type":"element","typeName":"SectionBlock","props":{"sections":[{"type":"element","typeName":"SectionItem","props":{"value":"landmarks","trigger":"Iconic Landmarks","content":[{"type":"element","typeName":"VisualCardBlock","props":{"items":[{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Eiffel Tower","subtext":"Open daily 9am–midnight","subtextVariant":"text","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"lm1body"},"id":"eiffel-tower","bgImageSrc":"https://c8.alamy.com/comp/RYEB13/aerial-view-of-the-eiffel-tower-with-the-park-champ-de-mars-and-the-river-seine-paris-france-RYEB13.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Must-See","icon":{"type":"element","typeName":"Icon","props":{"name":"star","category":"shapes"},"partial":true,"hasDynamicProps":false,"statementId":"lm1tagIcon"},"size":"sm","variant":"info"},"partial":true,"hasDynamicProps":false,"statementId":"lm1tag"},"bgImageAlt":"Aerial view of the Eiffel Tower"},"partial":true,"hasDynamicProps":false,"statementId":"lm1"},{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"The Louvre","subtext":"World's largest art museum","subtextVariant":"text","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"lm2body"},"id":"the-louvre","bgImageSrc":"https://images.fineartamerica.com/images/artworkimages/mediumlarge/3/1-exterior-of-musee-du-louvre-museum-paris-france-bernard-jaubert.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"8.9M visits/yr","icon":{"type":"element","typeName":"Icon","props":{"name":"trending-up","category":"charts"},"partial":true,"hasDynamicProps":false,"statementId":"lm2tagIcon"},"size":"sm","variant":"success"},"partial":true,"hasDynamicProps":false,"statementId":"lm2tag"},"bgImageAlt":"Exterior of the Louvre museum"},"partial":true,"hasDynamicProps":false,"statementId":"lm2"},{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Notre-Dame","subtext":"Fully restored cathedral","subtextVariant":"text","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"lm3body"},"id":"notre-dame","bgImageSrc":"https://cdn.britannica.com/29/255529-050-63A22A3C/notre-dame-de-paris-cathedral-paris-france.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Reopened 2024","icon":{"type":"element","typeName":"Icon","props":{"name":"hammer","category":"tools"},"partial":true,"hasDynamicProps":false,"statementId":"lm3tagIcon"},"size":"sm","variant":"warning"},"partial":true,"hasDynamicProps":false,"statementId":"lm3tag"},"bgImageAlt":"Notre-Dame cathedral facade"},"partial":true,"hasDynamicProps":false,"statementId":"lm3"},{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Palace of Versailles","subtext":"Royal gardens & grand halls","subtextVariant":"text","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"lm4body"},"id":"versailles","bgImageSrc":"https://cdn.britannica.com/76/130076-050-31128A3D/Latona-Fountain-Balthazard-Marsy-Gaspard-Andre-Le.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Day Trip","icon":{"type":"element","typeName":"Icon","props":{"name":"train-front","category":"travel"},"partial":true,"hasDynamicProps":false,"statementId":"lm4tagIcon"},"size":"sm","variant":"neutral"},"partial":true,"hasDynamicProps":false,"statementId":"lm4tag"},"bgImageAlt":"Latona Fountain at Versailles"},"partial":true,"hasDynamicProps":false,"statementId":"lm4"}],"layout":"carousel","responsive":true,"action":{"type":"continue_conversation","context":"Tell me more about this Paris landmark"}},"partial":true,"hasDynamicProps":false,"statementId":"landmarksCarousel"}]},"partial":true,"hasDynamicProps":false,"statementId":"secLandmarks"},{"type":"element","typeName":"SectionItem","props":{"value":"food","trigger":"Food & Drink","content":[{"type":"element","typeName":"ContextCardBlock","props":{"items":[{"type":"element","typeName":"ContextCardItem","props":{"id":"bistros","title":"Bistros","body":"Classic French fare — try Les Arlots for confit beef cheeks.","bgColor":"gray","bgImageSrc":"https://everydayparisian.com/wp-content/uploads/2023/01/IMG_0221-768x1024.webp"},"partial":true,"hasDynamicProps":false,"statementId":"f1"},{"type":"element","typeName":"ContextCardItem","props":{"id":"fine-dining","title":"Fine Dining","body":"Septime leads a new wave of creative Michelin-starred cuisine.","bgColor":"gray","bgImageSrc":"https://production-data.worldofmouth.app/images/72b2e6db-97ea-49c4-aa5d-3b7c8121c812.jpg"},"partial":true,"hasDynamicProps":false,"statementId":"f2"},{"type":"element","typeName":"ContextCardItem","props":{"id":"bakeries","title":"Bakeries","body":"Shinya Pain for sourdough. Grand Prix-winning baguettes citywide.","bgColor":"gray","bgImageSrc":"https://www.davidlebovitz.com/wp-content/uploads/2018/09/Le-petit-grain-paris-bakery-boulangerie-patisserie-pastry-shop-8-640x895.jpg"},"partial":true,"hasDynamicProps":false,"statementId":"f3"}],"layout":"grid","responsive":true,"action":{"type":"continue_conversation","context":"Tell me more about eating and drinking in Paris"}},"partial":true,"hasDynamicProps":false,"statementId":"foodCards"}]},"partial":true,"hasDynamicProps":false,"statementId":"secFood"},{"type":"element","typeName":"SectionItem","props":{"value":"when","trigger":"Best Time to Visit","content":[{"type":"element","typeName":"Tabs","props":{"items":[{"type":"element","typeName":"TabItem","props":{"value":"spring","trigger":"Spring","content":[{"type":"element","typeName":"ListBlock","props":{"items":[{"type":"element","typeName":"ListItem","props":{"title":"Cherry blossoms at Parc de Sceaux","subtitle":"March – May","image":null,"actionLabel":null,"action":{"type":"continue_conversation","context":"Tell me about cherry blossom season in Paris"}},"partial":true,"hasDynamicProps":false,"statementId":"sp1"},{"type":"element","typeName":"ListItem","props":{"title":"P"},"partial":true,"hasDynamicProps":false,"statementId":"sp2"}],"variant":"number"},"partial":true,"hasDynamicProps":false,"statementId":"springList"}]},"partial":true,"hasDynamicProps":false,"statementId":"tabSpring"}]},"partial":true,"hasDynamicProps":false,"statementId":"whenTabs"}]},"partial":true,"hasDynamicProps":false,"statementId":"secWhen"}],"isFoldable":false},"partial":true,"hasDynamicProps":false,"statementId":"sectionsBlock"}],"sources":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["sp3","tabSummer","actionBtns","src1","src2"],"orphaned":[],"statementCount":51,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":6070,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Paris","subtitle":"The City of Light · France's Eternal Capital"},"partial":true,"hasDynamicProps":false,"statementId":"header"},{"type":"element","typeName":"TextContent","props":{"text":"Paris is one of the world's most visited cities — a timeless blend of iconic landmarks, world-class cuisine, art, and romance [1]. In 2026 the city is buzzing with new energy: the **Grand Palais** has reopened after a €466M renovation, and you can now **swim in the Seine** for the first time in over a century [2]."},"partial":true,"hasDynamicProps":false,"statementId":"intro"},{"type":"element","typeName":"ContextCardBlock","props":{"items":[{"type":"element","typeName":"ContextCardItem","props":{"id":"art-museums","title":"Art & Museums","body":"143 museums including the Louvre & newly reopened Grand Palais.","bgColor":"gray","bgImageSrc":"https://cdn.britannica.com/03/121003-050-2544BD4E/Interior-Louvre-Museum-Paris.jpg"},"partial":true,"hasDynamicProps":false,"statementId":"h1"},{"type":"element","typeName":"ContextCardItem","props":{"id":"gastronomy","title":"Gastronomy","body":"9,000+ restaurants, 130+ Michelin stars, and the world's best baguettes.","bgColor":"gray"},"partial":true,"hasDynamicProps":false,"statementId":"h2"},{"type":"element","typeName":"ContextCardItem","props":{"id":"fashion-capital","title":"Fashion Capital","body":"Home to Chanel, Dior, Louis Vuitton & Hermès — the global luxury hub.","bgColor":"gray","bgImageSrc":"https://thumbs.dreamstime.com/z/dior-storefront-facade-chanel-store-lvmh-s-french-designer-modehouse-christian-adjacent-to-luxury-fashion-beauty-brand-327161553.jpg"},"partial":true,"hasDynamicProps":false,"statementId":"h3"}],"layout":"grid","responsive":true,"action":{"type":"continue_conversation","context":"Tell me more about this aspect of Paris"}},"partial":true,"hasDynamicProps":false,"statementId":"highlights"},{"type":"element","typeName":"InlineHeader","props":{"heading":"Paris by the Numbers","description":"Key visitor statistics"},"partial":true,"hasDynamicProps":false,"statementId":"statsHeader"},{"type":"element","typeName":"OverviewCardBlock","props":{"items":[{"type":"element","typeName":"OverviewCardItem","props":{"id":"annual-visitors","top":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"users","category":"people"},"partial":true,"hasDynamicProps":false,"statementId":"ov1icon"},"iconVariant":"neutral","iconSize":"m","title":"Annual Visitors","subtitle":"2024 arrivals","bold":false,"layout":"vertical"},"partial":true,"hasDynamicProps":false,"statementId":"ov1top"},"bottom":{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"48.7M","subtext":"tourists","trend":{"direction":"up","value":2.5}},"partial":true,"hasDynamicProps":false,"statementId":"ov1metric"}},"partial":true,"hasDynamicProps":false,"statementId":"ov1"},{"type":"element","typeName":"OverviewCardItem","props":{"id":"museums-monuments","top":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"landmark","category":"buildings"},"partial":true,"hasDynamicProps":false,"statementId":"ov2icon"},"iconVariant":"neutral","iconSize":"m","title":"Museums & Monuments","subtitle":"Cultural sites","bold":false,"layout":"vertical"},"partial":true,"hasDynamicProps":false,"statementId":"ov2top"},"bottom":{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"2,370+","subtext":"across the city"},"partial":true,"hasDynamicProps":false,"statementId":"ov2metric"}},"partial":true,"hasDynamicProps":false,"statementId":"ov2"},{"type":"element","typeName":"OverviewCardItem","props":{"id":"tourism-revenue","top":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"circle-dollar-sign","category":"finance"},"partial":true,"hasDynamicProps":false,"statementId":"ov3icon"},"iconVariant":"neutral","iconSize":"m","title":"Tourism Revenue","subtitle":"2024 estimate","bold":false,"layout":"vertical"},"partial":true,"hasDynamicProps":false,"statementId":"ov3top"},"bottom":{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"€23.4B","subtext":"generated"},"partial":true,"hasDynamicProps":false,"statementId":"ov3metric"}},"partial":true,"hasDynamicProps":false,"statementId":"ov3"}],"layout":"grid","responsive":true},"partial":true,"hasDynamicProps":false,"statementId":"statsCards"},{"type":"element","typeName":"SectionBlock","props":{"sections":[{"type":"element","typeName":"SectionItem","props":{"value":"landmarks","trigger":"Iconic Landmarks","content":[{"type":"element","typeName":"VisualCardBlock","props":{"items":[{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Eiffel Tower","subtext":"Open daily 9am–midnight","subtextVariant":"text","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"lm1body"},"id":"eiffel-tower","bgImageSrc":"https://c8.alamy.com/comp/RYEB13/aerial-view-of-the-eiffel-tower-with-the-park-champ-de-mars-and-the-river-seine-paris-france-RYEB13.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Must-See","icon":{"type":"element","typeName":"Icon","props":{"name":"star","category":"shapes"},"partial":true,"hasDynamicProps":false,"statementId":"lm1tagIcon"},"size":"sm","variant":"info"},"partial":true,"hasDynamicProps":false,"statementId":"lm1tag"},"bgImageAlt":"Aerial view of the Eiffel Tower"},"partial":true,"hasDynamicProps":false,"statementId":"lm1"},{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"The Louvre","subtext":"World's largest art museum","subtextVariant":"text","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"lm2body"},"id":"the-louvre","bgImageSrc":"https://images.fineartamerica.com/images/artworkimages/mediumlarge/3/1-exterior-of-musee-du-louvre-museum-paris-france-bernard-jaubert.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"8.9M visits/yr","icon":{"type":"element","typeName":"Icon","props":{"name":"trending-up","category":"charts"},"partial":true,"hasDynamicProps":false,"statementId":"lm2tagIcon"},"size":"sm","variant":"success"},"partial":true,"hasDynamicProps":false,"statementId":"lm2tag"},"bgImageAlt":"Exterior of the Louvre museum"},"partial":true,"hasDynamicProps":false,"statementId":"lm2"},{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Notre-Dame","subtext":"Fully restored cathedral","subtextVariant":"text","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"lm3body"},"id":"notre-dame","bgImageSrc":"https://cdn.britannica.com/29/255529-050-63A22A3C/notre-dame-de-paris-cathedral-paris-france.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Reopened 2024","icon":{"type":"element","typeName":"Icon","props":{"name":"hammer","category":"tools"},"partial":true,"hasDynamicProps":false,"statementId":"lm3tagIcon"},"size":"sm","variant":"warning"},"partial":true,"hasDynamicProps":false,"statementId":"lm3tag"},"bgImageAlt":"Notre-Dame cathedral facade"},"partial":true,"hasDynamicProps":false,"statementId":"lm3"},{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Palace of Versailles","subtext":"Royal gardens & grand halls","subtextVariant":"text","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"lm4body"},"id":"versailles","bgImageSrc":"https://cdn.britannica.com/76/130076-050-31128A3D/Latona-Fountain-Balthazard-Marsy-Gaspard-Andre-Le.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Day Trip","icon":{"type":"element","typeName":"Icon","props":{"name":"train-front","category":"travel"},"partial":true,"hasDynamicProps":false,"statementId":"lm4tagIcon"},"size":"sm","variant":"neutral"},"partial":true,"hasDynamicProps":false,"statementId":"lm4tag"},"bgImageAlt":"Latona Fountain at Versailles"},"partial":true,"hasDynamicProps":false,"statementId":"lm4"}],"layout":"carousel","responsive":true,"action":{"type":"continue_conversation","context":"Tell me more about this Paris landmark"}},"partial":true,"hasDynamicProps":false,"statementId":"landmarksCarousel"}]},"partial":true,"hasDynamicProps":false,"statementId":"secLandmarks"},{"type":"element","typeName":"SectionItem","props":{"value":"food","trigger":"Food & Drink","content":[{"type":"element","typeName":"ContextCardBlock","props":{"items":[{"type":"element","typeName":"ContextCardItem","props":{"id":"bistros","title":"Bistros","body":"Classic French fare — try Les Arlots for confit beef cheeks.","bgColor":"gray","bgImageSrc":"https://everydayparisian.com/wp-content/uploads/2023/01/IMG_0221-768x1024.webp"},"partial":true,"hasDynamicProps":false,"statementId":"f1"},{"type":"element","typeName":"ContextCardItem","props":{"id":"fine-dining","title":"Fine Dining","body":"Septime leads a new wave of creative Michelin-starred cuisine.","bgColor":"gray","bgImageSrc":"https://production-data.worldofmouth.app/images/72b2e6db-97ea-49c4-aa5d-3b7c8121c812.jpg"},"partial":true,"hasDynamicProps":false,"statementId":"f2"},{"type":"element","typeName":"ContextCardItem","props":{"id":"bakeries","title":"Bakeries","body":"Shinya Pain for sourdough. Grand Prix-winning baguettes citywide.","bgColor":"gray","bgImageSrc":"https://www.davidlebovitz.com/wp-content/uploads/2018/09/Le-petit-grain-paris-bakery-boulangerie-patisserie-pastry-shop-8-640x895.jpg"},"partial":true,"hasDynamicProps":false,"statementId":"f3"}],"layout":"grid","responsive":true,"action":{"type":"continue_conversation","context":"Tell me more about eating and drinking in Paris"}},"partial":true,"hasDynamicProps":false,"statementId":"foodCards"}]},"partial":true,"hasDynamicProps":false,"statementId":"secFood"},{"type":"element","typeName":"SectionItem","props":{"value":"when","trigger":"Best Time to Visit","content":[{"type":"element","typeName":"Tabs","props":{"items":[{"type":"element","typeName":"TabItem","props":{"value":"spring","trigger":"Spring","content":[{"type":"element","typeName":"ListBlock","props":{"items":[{"type":"element","typeName":"ListItem","props":{"title":"Cherry blossoms at Parc de Sceaux","subtitle":"March – May","image":null,"actionLabel":null,"action":{"type":"continue_conversation","context":"Tell me about cherry blossom season in Paris"}},"partial":true,"hasDynamicProps":false,"statementId":"sp1"},{"type":"element","typeName":"ListItem","props":{"title":"Picnics along the Seine & in city parks","subtitle":"Warm, lively atmosphere","image":null,"actionLabel":null,"action":{"type":"continue_conversation","context":"Where are the best picnic spots in Paris?"}},"partial":true,"hasDynamicProps":false,"statementId":"sp2"},{"type":"element","typeName":"ListItem","props":{"title":"Fewer crowds than summer","subtitle":"Ideal for sightseeing","image":null,"actionLabel":null,"action":{"type":"continue_conversation","context":"How busy is Paris in spring?"}},"partial":true,"hasDynamicProps":false,"statementId":"sp3"}],"variant":"number"},"partial":true,"hasDynamicProps":false,"statementId":"springList"}]},"partial":true,"hasDynamicProps":false,"statementId":"tabSpring"},{"type":"element","typeName":"TabItem","props":{"value":"summer","trigger":"Summer","content":[{"type":"element","typeName":"ListBlock","props":{"items":[{"type":"element","typeName":"ListItem","props":{"title":"Swim in the Seine — open July & August 2026","subtitle":"First time in 102 years","image":null,"actionLabel":null,"action":{"type":"continue_conversation","context":"Tell me about swimming in the Sein"}},"partial":true,"hasDynamicProps":false,"statementId":"su1"}],"variant":"number"},"partial":true,"hasDynamicProps":false,"statementId":"summerList"}]},"partial":true,"hasDynamicProps":false,"statementId":"tabSummer"}]},"partial":true,"hasDynamicProps":false,"statementId":"whenTabs"}]},"partial":true,"hasDynamicProps":false,"statementId":"secWhen"}],"isFoldable":false},"partial":true,"hasDynamicProps":false,"statementId":"sectionsBlock"}],"sources":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["su2","su3","actionBtns","src1","src2"],"orphaned":[],"statementCount":55,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":6677,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Paris","subtitle":"The City of Light · France's Eternal Capital"},"partial":true,"hasDynamicProps":false,"statementId":"header"},{"type":"element","typeName":"TextContent","props":{"text":"Paris is one of the world's most visited cities — a timeless blend of iconic landmarks, world-class cuisine, art, and romance [1]. In 2026 the city is buzzing with new energy: the **Grand Palais** has reopened after a €466M renovation, and you can now **swim in the Seine** for the first time in over a century [2]."},"partial":true,"hasDynamicProps":false,"statementId":"intro"},{"type":"element","typeName":"ContextCardBlock","props":{"items":[{"type":"element","typeName":"ContextCardItem","props":{"id":"art-museums","title":"Art & Museums","body":"143 museums including the Louvre & newly reopened Grand Palais.","bgColor":"gray","bgImageSrc":"https://cdn.britannica.com/03/121003-050-2544BD4E/Interior-Louvre-Museum-Paris.jpg"},"partial":true,"hasDynamicProps":false,"statementId":"h1"},{"type":"element","typeName":"ContextCardItem","props":{"id":"gastronomy","title":"Gastronomy","body":"9,000+ restaurants, 130+ Michelin stars, and the world's best baguettes.","bgColor":"gray"},"partial":true,"hasDynamicProps":false,"statementId":"h2"},{"type":"element","typeName":"ContextCardItem","props":{"id":"fashion-capital","title":"Fashion Capital","body":"Home to Chanel, Dior, Louis Vuitton & Hermès — the global luxury hub.","bgColor":"gray","bgImageSrc":"https://thumbs.dreamstime.com/z/dior-storefront-facade-chanel-store-lvmh-s-french-designer-modehouse-christian-adjacent-to-luxury-fashion-beauty-brand-327161553.jpg"},"partial":true,"hasDynamicProps":false,"statementId":"h3"}],"layout":"grid","responsive":true,"action":{"type":"continue_conversation","context":"Tell me more about this aspect of Paris"}},"partial":true,"hasDynamicProps":false,"statementId":"highlights"},{"type":"element","typeName":"InlineHeader","props":{"heading":"Paris by the Numbers","description":"Key visitor statistics"},"partial":true,"hasDynamicProps":false,"statementId":"statsHeader"},{"type":"element","typeName":"OverviewCardBlock","props":{"items":[{"type":"element","typeName":"OverviewCardItem","props":{"id":"annual-visitors","top":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"users","category":"people"},"partial":true,"hasDynamicProps":false,"statementId":"ov1icon"},"iconVariant":"neutral","iconSize":"m","title":"Annual Visitors","subtitle":"2024 arrivals","bold":false,"layout":"vertical"},"partial":true,"hasDynamicProps":false,"statementId":"ov1top"},"bottom":{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"48.7M","subtext":"tourists","trend":{"direction":"up","value":2.5}},"partial":true,"hasDynamicProps":false,"statementId":"ov1metric"}},"partial":true,"hasDynamicProps":false,"statementId":"ov1"},{"type":"element","typeName":"OverviewCardItem","props":{"id":"museums-monuments","top":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"landmark","category":"buildings"},"partial":true,"hasDynamicProps":false,"statementId":"ov2icon"},"iconVariant":"neutral","iconSize":"m","title":"Museums & Monuments","subtitle":"Cultural sites","bold":false,"layout":"vertical"},"partial":true,"hasDynamicProps":false,"statementId":"ov2top"},"bottom":{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"2,370+","subtext":"across the city"},"partial":true,"hasDynamicProps":false,"statementId":"ov2metric"}},"partial":true,"hasDynamicProps":false,"statementId":"ov2"},{"type":"element","typeName":"OverviewCardItem","props":{"id":"tourism-revenue","top":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"circle-dollar-sign","category":"finance"},"partial":true,"hasDynamicProps":false,"statementId":"ov3icon"},"iconVariant":"neutral","iconSize":"m","title":"Tourism Revenue","subtitle":"2024 estimate","bold":false,"layout":"vertical"},"partial":true,"hasDynamicProps":false,"statementId":"ov3top"},"bottom":{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"€23.4B","subtext":"generated"},"partial":true,"hasDynamicProps":false,"statementId":"ov3metric"}},"partial":true,"hasDynamicProps":false,"statementId":"ov3"}],"layout":"grid","responsive":true},"partial":true,"hasDynamicProps":false,"statementId":"statsCards"},{"type":"element","typeName":"SectionBlock","props":{"sections":[{"type":"element","typeName":"SectionItem","props":{"value":"landmarks","trigger":"Iconic Landmarks","content":[{"type":"element","typeName":"VisualCardBlock","props":{"items":[{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Eiffel Tower","subtext":"Open daily 9am–midnight","subtextVariant":"text","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"lm1body"},"id":"eiffel-tower","bgImageSrc":"https://c8.alamy.com/comp/RYEB13/aerial-view-of-the-eiffel-tower-with-the-park-champ-de-mars-and-the-river-seine-paris-france-RYEB13.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Must-See","icon":{"type":"element","typeName":"Icon","props":{"name":"star","category":"shapes"},"partial":true,"hasDynamicProps":false,"statementId":"lm1tagIcon"},"size":"sm","variant":"info"},"partial":true,"hasDynamicProps":false,"statementId":"lm1tag"},"bgImageAlt":"Aerial view of the Eiffel Tower"},"partial":true,"hasDynamicProps":false,"statementId":"lm1"},{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"The Louvre","subtext":"World's largest art museum","subtextVariant":"text","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"lm2body"},"id":"the-louvre","bgImageSrc":"https://images.fineartamerica.com/images/artworkimages/mediumlarge/3/1-exterior-of-musee-du-louvre-museum-paris-france-bernard-jaubert.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"8.9M visits/yr","icon":{"type":"element","typeName":"Icon","props":{"name":"trending-up","category":"charts"},"partial":true,"hasDynamicProps":false,"statementId":"lm2tagIcon"},"size":"sm","variant":"success"},"partial":true,"hasDynamicProps":false,"statementId":"lm2tag"},"bgImageAlt":"Exterior of the Louvre museum"},"partial":true,"hasDynamicProps":false,"statementId":"lm2"},{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Notre-Dame","subtext":"Fully restored cathedral","subtextVariant":"text","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"lm3body"},"id":"notre-dame","bgImageSrc":"https://cdn.britannica.com/29/255529-050-63A22A3C/notre-dame-de-paris-cathedral-paris-france.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Reopened 2024","icon":{"type":"element","typeName":"Icon","props":{"name":"hammer","category":"tools"},"partial":true,"hasDynamicProps":false,"statementId":"lm3tagIcon"},"size":"sm","variant":"warning"},"partial":true,"hasDynamicProps":false,"statementId":"lm3tag"},"bgImageAlt":"Notre-Dame cathedral facade"},"partial":true,"hasDynamicProps":false,"statementId":"lm3"},{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Palace of Versailles","subtext":"Royal gardens & grand halls","subtextVariant":"text","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"lm4body"},"id":"versailles","bgImageSrc":"https://cdn.britannica.com/76/130076-050-31128A3D/Latona-Fountain-Balthazard-Marsy-Gaspard-Andre-Le.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Day Trip","icon":{"type":"element","typeName":"Icon","props":{"name":"train-front","category":"travel"},"partial":true,"hasDynamicProps":false,"statementId":"lm4tagIcon"},"size":"sm","variant":"neutral"},"partial":true,"hasDynamicProps":false,"statementId":"lm4tag"},"bgImageAlt":"Latona Fountain at Versailles"},"partial":true,"hasDynamicProps":false,"statementId":"lm4"}],"layout":"carousel","responsive":true,"action":{"type":"continue_conversation","context":"Tell me more about this Paris landmark"}},"partial":true,"hasDynamicProps":false,"statementId":"landmarksCarousel"}]},"partial":true,"hasDynamicProps":false,"statementId":"secLandmarks"},{"type":"element","typeName":"SectionItem","props":{"value":"food","trigger":"Food & Drink","content":[{"type":"element","typeName":"ContextCardBlock","props":{"items":[{"type":"element","typeName":"ContextCardItem","props":{"id":"bistros","title":"Bistros","body":"Classic French fare — try Les Arlots for confit beef cheeks.","bgColor":"gray","bgImageSrc":"https://everydayparisian.com/wp-content/uploads/2023/01/IMG_0221-768x1024.webp"},"partial":true,"hasDynamicProps":false,"statementId":"f1"},{"type":"element","typeName":"ContextCardItem","props":{"id":"fine-dining","title":"Fine Dining","body":"Septime leads a new wave of creative Michelin-starred cuisine.","bgColor":"gray","bgImageSrc":"https://production-data.worldofmouth.app/images/72b2e6db-97ea-49c4-aa5d-3b7c8121c812.jpg"},"partial":true,"hasDynamicProps":false,"statementId":"f2"},{"type":"element","typeName":"ContextCardItem","props":{"id":"bakeries","title":"Bakeries","body":"Shinya Pain for sourdough. Grand Prix-winning baguettes citywide.","bgColor":"gray","bgImageSrc":"https://www.davidlebovitz.com/wp-content/uploads/2018/09/Le-petit-grain-paris-bakery-boulangerie-patisserie-pastry-shop-8-640x895.jpg"},"partial":true,"hasDynamicProps":false,"statementId":"f3"}],"layout":"grid","responsive":true,"action":{"type":"continue_conversation","context":"Tell me more about eating and drinking in Paris"}},"partial":true,"hasDynamicProps":false,"statementId":"foodCards"}]},"partial":true,"hasDynamicProps":false,"statementId":"secFood"},{"type":"element","typeName":"SectionItem","props":{"value":"when","trigger":"Best Time to Visit","content":[{"type":"element","typeName":"Tabs","props":{"items":[{"type":"element","typeName":"TabItem","props":{"value":"spring","trigger":"Spring","content":[{"type":"element","typeName":"ListBlock","props":{"items":[{"type":"element","typeName":"ListItem","props":{"title":"Cherry blossoms at Parc de Sceaux","subtitle":"March – May","image":null,"actionLabel":null,"action":{"type":"continue_conversation","context":"Tell me about cherry blossom season in Paris"}},"partial":true,"hasDynamicProps":false,"statementId":"sp1"},{"type":"element","typeName":"ListItem","props":{"title":"Picnics along the Seine & in city parks","subtitle":"Warm, lively atmosphere","image":null,"actionLabel":null,"action":{"type":"continue_conversation","context":"Where are the best picnic spots in Paris?"}},"partial":true,"hasDynamicProps":false,"statementId":"sp2"},{"type":"element","typeName":"ListItem","props":{"title":"Fewer crowds than summer","subtitle":"Ideal for sightseeing","image":null,"actionLabel":null,"action":{"type":"continue_conversation","context":"How busy is Paris in spring?"}},"partial":true,"hasDynamicProps":false,"statementId":"sp3"}],"variant":"number"},"partial":true,"hasDynamicProps":false,"statementId":"springList"}]},"partial":true,"hasDynamicProps":false,"statementId":"tabSpring"},{"type":"element","typeName":"TabItem","props":{"value":"summer","trigger":"Summer","content":[{"type":"element","typeName":"ListBlock","props":{"items":[{"type":"element","typeName":"ListItem","props":{"title":"Swim in the Seine — open July & August 2026","subtitle":"First time in 102 years","image":null,"actionLabel":null,"action":{"type":"continue_conversation","context":"Tell me about swimming in the Seine"}},"partial":true,"hasDynamicProps":false,"statementId":"su1"},{"type":"element","typeName":"ListItem","props":{"title":"Open-air cinema at Parc de la Villette","subtitle":"Free films under the stars","image":null,"actionLabel":null,"action":{"type":"continue_conversation","context":"Tell me about the open-air cinema at Parc de la Villette"}},"partial":true,"hasDynamicProps":false,"statementId":"su2"},{"type":"element","typeName":"ListItem","props":{"title":"Bastille Day celebrations on July 14","subtitle":"Fireworks at the Eiffel Tower","image":null,"actionLabel":null,"action":{"type":"continue_conversation","context":"What happens in Paris on Bastille Day?"}},"partial":true,"hasDynamicProps":false,"statementId":"su3"}],"variant":"number"},"partial":true,"hasDynamicProps":false,"statementId":"summerList"}]},"partial":true,"hasDynamicProps":false,"statementId":"tabSummer"}]},"partial":true,"hasDynamicProps":false,"statementId":"whenTabs"}]},"partial":true,"hasDynamicProps":false,"statementId":"secWhen"}],"isFoldable":false},"partial":true,"hasDynamicProps":false,"statementId":"sectionsBlock"},{"type":"element","typeName":"Buttons","props":{"buttons":[{"type":"element","typeName":"Button","props":{"label":"Plan a Trip to Paris","action":{"type":"continue_conversation","context":"Help me plan a trip to Paris"},"variant":"primary"},"partial":true,"hasDynamicProps":false,"statementId":"btn1"},{"type":"element","typeName":"Button","props":{"label":"Best Hotels in Paris","action":{}},"partial":true,"hasDynamicProps":false,"statementId":"btn2"}],"direction":"row"},"partial":true,"hasDynamicProps":false,"statementId":"actionBtns"}],"sources":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["btn3","src1","src2"],"orphaned":[],"statementCount":60,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":7277,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Paris","subtitle":"The City of Light · France's Eternal Capital"},"partial":false,"hasDynamicProps":false,"statementId":"header"},{"type":"element","typeName":"TextContent","props":{"text":"Paris is one of the world's most visited cities — a timeless blend of iconic landmarks, world-class cuisine, art, and romance [1]. In 2026 the city is buzzing with new energy: the **Grand Palais** has reopened after a €466M renovation, and you can now **swim in the Seine** for the first time in over a century [2]."},"partial":false,"hasDynamicProps":false,"statementId":"intro"},{"type":"element","typeName":"ContextCardBlock","props":{"items":[{"type":"element","typeName":"ContextCardItem","props":{"id":"art-museums","title":"Art & Museums","body":"143 museums including the Louvre & newly reopened Grand Palais.","bgColor":"gray","bgImageSrc":"https://cdn.britannica.com/03/121003-050-2544BD4E/Interior-Louvre-Museum-Paris.jpg"},"partial":false,"hasDynamicProps":false,"statementId":"h1"},{"type":"element","typeName":"ContextCardItem","props":{"id":"gastronomy","title":"Gastronomy","body":"9,000+ restaurants, 130+ Michelin stars, and the world's best baguettes.","bgColor":"gray"},"partial":false,"hasDynamicProps":false,"statementId":"h2"},{"type":"element","typeName":"ContextCardItem","props":{"id":"fashion-capital","title":"Fashion Capital","body":"Home to Chanel, Dior, Louis Vuitton & Hermès — the global luxury hub.","bgColor":"gray","bgImageSrc":"https://thumbs.dreamstime.com/z/dior-storefront-facade-chanel-store-lvmh-s-french-designer-modehouse-christian-adjacent-to-luxury-fashion-beauty-brand-327161553.jpg"},"partial":false,"hasDynamicProps":false,"statementId":"h3"}],"layout":"grid","responsive":true,"action":{"type":"continue_conversation","context":"Tell me more about this aspect of Paris"}},"partial":false,"hasDynamicProps":false,"statementId":"highlights"},{"type":"element","typeName":"InlineHeader","props":{"heading":"Paris by the Numbers","description":"Key visitor statistics"},"partial":false,"hasDynamicProps":false,"statementId":"statsHeader"},{"type":"element","typeName":"OverviewCardBlock","props":{"items":[{"type":"element","typeName":"OverviewCardItem","props":{"id":"annual-visitors","top":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"users","category":"people"},"partial":false,"hasDynamicProps":false,"statementId":"ov1icon"},"iconVariant":"neutral","iconSize":"m","title":"Annual Visitors","subtitle":"2024 arrivals","bold":false,"layout":"vertical"},"partial":false,"hasDynamicProps":false,"statementId":"ov1top"},"bottom":{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"48.7M","subtext":"tourists","trend":{"direction":"up","value":2.5}},"partial":false,"hasDynamicProps":false,"statementId":"ov1metric"}},"partial":false,"hasDynamicProps":false,"statementId":"ov1"},{"type":"element","typeName":"OverviewCardItem","props":{"id":"museums-monuments","top":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"landmark","category":"buildings"},"partial":false,"hasDynamicProps":false,"statementId":"ov2icon"},"iconVariant":"neutral","iconSize":"m","title":"Museums & Monuments","subtitle":"Cultural sites","bold":false,"layout":"vertical"},"partial":false,"hasDynamicProps":false,"statementId":"ov2top"},"bottom":{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"2,370+","subtext":"across the city"},"partial":false,"hasDynamicProps":false,"statementId":"ov2metric"}},"partial":false,"hasDynamicProps":false,"statementId":"ov2"},{"type":"element","typeName":"OverviewCardItem","props":{"id":"tourism-revenue","top":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"circle-dollar-sign","category":"finance"},"partial":false,"hasDynamicProps":false,"statementId":"ov3icon"},"iconVariant":"neutral","iconSize":"m","title":"Tourism Revenue","subtitle":"2024 estimate","bold":false,"layout":"vertical"},"partial":false,"hasDynamicProps":false,"statementId":"ov3top"},"bottom":{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"€23.4B","subtext":"generated"},"partial":false,"hasDynamicProps":false,"statementId":"ov3metric"}},"partial":false,"hasDynamicProps":false,"statementId":"ov3"}],"layout":"grid","responsive":true},"partial":false,"hasDynamicProps":false,"statementId":"statsCards"},{"type":"element","typeName":"SectionBlock","props":{"sections":[{"type":"element","typeName":"SectionItem","props":{"value":"landmarks","trigger":"Iconic Landmarks","content":[{"type":"element","typeName":"VisualCardBlock","props":{"items":[{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Eiffel Tower","subtext":"Open daily 9am–midnight","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"lm1body"},"id":"eiffel-tower","bgImageSrc":"https://c8.alamy.com/comp/RYEB13/aerial-view-of-the-eiffel-tower-with-the-park-champ-de-mars-and-the-river-seine-paris-france-RYEB13.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Must-See","icon":{"type":"element","typeName":"Icon","props":{"name":"star","category":"shapes"},"partial":false,"hasDynamicProps":false,"statementId":"lm1tagIcon"},"size":"sm","variant":"info"},"partial":false,"hasDynamicProps":false,"statementId":"lm1tag"},"bgImageAlt":"Aerial view of the Eiffel Tower"},"partial":false,"hasDynamicProps":false,"statementId":"lm1"},{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"The Louvre","subtext":"World's largest art museum","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"lm2body"},"id":"the-louvre","bgImageSrc":"https://images.fineartamerica.com/images/artworkimages/mediumlarge/3/1-exterior-of-musee-du-louvre-museum-paris-france-bernard-jaubert.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"8.9M visits/yr","icon":{"type":"element","typeName":"Icon","props":{"name":"trending-up","category":"charts"},"partial":false,"hasDynamicProps":false,"statementId":"lm2tagIcon"},"size":"sm","variant":"success"},"partial":false,"hasDynamicProps":false,"statementId":"lm2tag"},"bgImageAlt":"Exterior of the Louvre museum"},"partial":false,"hasDynamicProps":false,"statementId":"lm2"},{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Notre-Dame","subtext":"Fully restored cathedral","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"lm3body"},"id":"notre-dame","bgImageSrc":"https://cdn.britannica.com/29/255529-050-63A22A3C/notre-dame-de-paris-cathedral-paris-france.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Reopened 2024","icon":{"type":"element","typeName":"Icon","props":{"name":"hammer","category":"tools"},"partial":false,"hasDynamicProps":false,"statementId":"lm3tagIcon"},"size":"sm","variant":"warning"},"partial":false,"hasDynamicProps":false,"statementId":"lm3tag"},"bgImageAlt":"Notre-Dame cathedral facade"},"partial":false,"hasDynamicProps":false,"statementId":"lm3"},{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Palace of Versailles","subtext":"Royal gardens & grand halls","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"lm4body"},"id":"versailles","bgImageSrc":"https://cdn.britannica.com/76/130076-050-31128A3D/Latona-Fountain-Balthazard-Marsy-Gaspard-Andre-Le.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Day Trip","icon":{"type":"element","typeName":"Icon","props":{"name":"train-front","category":"travel"},"partial":false,"hasDynamicProps":false,"statementId":"lm4tagIcon"},"size":"sm","variant":"neutral"},"partial":false,"hasDynamicProps":false,"statementId":"lm4tag"},"bgImageAlt":"Latona Fountain at Versailles"},"partial":false,"hasDynamicProps":false,"statementId":"lm4"}],"layout":"carousel","responsive":true,"action":{"type":"continue_conversation","context":"Tell me more about this Paris landmark"}},"partial":false,"hasDynamicProps":false,"statementId":"landmarksCarousel"}]},"partial":false,"hasDynamicProps":false,"statementId":"secLandmarks"},{"type":"element","typeName":"SectionItem","props":{"value":"food","trigger":"Food & Drink","content":[{"type":"element","typeName":"ContextCardBlock","props":{"items":[{"type":"element","typeName":"ContextCardItem","props":{"id":"bistros","title":"Bistros","body":"Classic French fare — try Les Arlots for confit beef cheeks.","bgColor":"gray","bgImageSrc":"https://everydayparisian.com/wp-content/uploads/2023/01/IMG_0221-768x1024.webp"},"partial":false,"hasDynamicProps":false,"statementId":"f1"},{"type":"element","typeName":"ContextCardItem","props":{"id":"fine-dining","title":"Fine Dining","body":"Septime leads a new wave of creative Michelin-starred cuisine.","bgColor":"gray","bgImageSrc":"https://production-data.worldofmouth.app/images/72b2e6db-97ea-49c4-aa5d-3b7c8121c812.jpg"},"partial":false,"hasDynamicProps":false,"statementId":"f2"},{"type":"element","typeName":"ContextCardItem","props":{"id":"bakeries","title":"Bakeries","body":"Shinya Pain for sourdough. Grand Prix-winning baguettes citywide.","bgColor":"gray","bgImageSrc":"https://www.davidlebovitz.com/wp-content/uploads/2018/09/Le-petit-grain-paris-bakery-boulangerie-patisserie-pastry-shop-8-640x895.jpg"},"partial":false,"hasDynamicProps":false,"statementId":"f3"}],"layout":"grid","responsive":true,"action":{"type":"continue_conversation","context":"Tell me more about eating and drinking in Paris"}},"partial":false,"hasDynamicProps":false,"statementId":"foodCards"}]},"partial":false,"hasDynamicProps":false,"statementId":"secFood"},{"type":"element","typeName":"SectionItem","props":{"value":"when","trigger":"Best Time to Visit","content":[{"type":"element","typeName":"Tabs","props":{"items":[{"type":"element","typeName":"TabItem","props":{"value":"spring","trigger":"Spring","content":[{"type":"element","typeName":"ListBlock","props":{"items":[{"type":"element","typeName":"ListItem","props":{"title":"Cherry blossoms at Parc de Sceaux","subtitle":"March – May","image":null,"actionLabel":null,"action":{"type":"continue_conversation","context":"Tell me about cherry blossom season in Paris"}},"partial":false,"hasDynamicProps":false,"statementId":"sp1"},{"type":"element","typeName":"ListItem","props":{"title":"Picnics along the Seine & in city parks","subtitle":"Warm, lively atmosphere","image":null,"actionLabel":null,"action":{"type":"continue_conversation","context":"Where are the best picnic spots in Paris?"}},"partial":false,"hasDynamicProps":false,"statementId":"sp2"},{"type":"element","typeName":"ListItem","props":{"title":"Fewer crowds than summer","subtitle":"Ideal for sightseeing","image":null,"actionLabel":null,"action":{"type":"continue_conversation","context":"How busy is Paris in spring?"}},"partial":false,"hasDynamicProps":false,"statementId":"sp3"}],"variant":"number"},"partial":false,"hasDynamicProps":false,"statementId":"springList"}]},"partial":false,"hasDynamicProps":false,"statementId":"tabSpring"},{"type":"element","typeName":"TabItem","props":{"value":"summer","trigger":"Summer","content":[{"type":"element","typeName":"ListBlock","props":{"items":[{"type":"element","typeName":"ListItem","props":{"title":"Swim in the Seine — open July & August 2026","subtitle":"First time in 102 years","image":null,"actionLabel":null,"action":{"type":"continue_conversation","context":"Tell me about swimming in the Seine"}},"partial":false,"hasDynamicProps":false,"statementId":"su1"},{"type":"element","typeName":"ListItem","props":{"title":"Open-air cinema at Parc de la Villette","subtitle":"Free films under the stars","image":null,"actionLabel":null,"action":{"type":"continue_conversation","context":"Tell me about the open-air cinema at Parc de la Villette"}},"partial":false,"hasDynamicProps":false,"statementId":"su2"},{"type":"element","typeName":"ListItem","props":{"title":"Bastille Day celebrations on July 14","subtitle":"Fireworks at the Eiffel Tower","image":null,"actionLabel":null,"action":{"type":"continue_conversation","context":"What happens in Paris on Bastille Day?"}},"partial":false,"hasDynamicProps":false,"statementId":"su3"}],"variant":"number"},"partial":false,"hasDynamicProps":false,"statementId":"summerList"}]},"partial":false,"hasDynamicProps":false,"statementId":"tabSummer"}]},"partial":false,"hasDynamicProps":false,"statementId":"whenTabs"}]},"partial":false,"hasDynamicProps":false,"statementId":"secWhen"}],"isFoldable":false},"partial":false,"hasDynamicProps":false,"statementId":"sectionsBlock"},{"type":"element","typeName":"Buttons","props":{"buttons":[{"type":"element","typeName":"Button","props":{"label":"Plan a Trip to Paris","action":{"type":"continue_conversation","context":"Help me plan a trip to Paris"},"variant":"primary"},"partial":false,"hasDynamicProps":false,"statementId":"btn1"},{"type":"element","typeName":"Button","props":{"label":"Best Hotels in Paris","action":{"type":"continue_conversation","context":"Show me the best hotels in Paris"},"variant":"secondary"},"partial":false,"hasDynamicProps":false,"statementId":"btn2"},{"type":"element","typeName":"Button","props":{"label":"Build a Paris Itinerary","action":{"type":"continue_conversation","context":"Build a 5-day Paris itinerary for me"},"variant":"secondary"},"partial":false,"hasDynamicProps":false,"statementId":"btn3"}],"direction":"row"},"partial":false,"hasDynamicProps":false,"statementId":"actionBtns"}],"sources":[{"title":"Paris Travel Guide 2025","sourceName":"Travel and Tour World","url":"https://www.travelandtourworld.com/news/article/from-iconic-landmarks-to-hidden-gems-the-ultimate-paris-travel-guide-for-2025-you-need-to-read-now/"},{"title":"Best Things to Do in Paris 2026","sourceName":"Time Out Paris","url":"http://www.timeout.fr/paris/en/for-tourists"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":63,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"chat-example-2","schema":"chat","input":"Example 2 — Data and analytics response (quantitative asks lead with KPIs and charts):\n\nroot = Card([pageHeader, kpiBlock, sections, deepDiveHeader, compositeCards, actionButtons])\npageHeader = CardHeader(\"Analytics Dashboard\", \"April 2026 · Business Performance Overview\")\nkpiBlock = SnippetCardBlock([kpi1, kpi2, kpi3, kpi4], \"grid\", true)\nkpi1 = SnippetCardItem(\"total-revenue\", kpi1lhs, kpi1rhs)\nkpi1lhs = IconText(kpi1icon, \"neutral\", \"m\", \"Total Revenue\", \"Month to date\", false, \"horizontal\")\nkpi1icon = Icon(\"circle-dollar-sign\", \"finance\")\nkpi1rhs = BoldText(\"number\", \"$284,500\", \"+12.4%\", \"metric\")\nkpi2 = SnippetCardItem(\"active-users\", kpi2lhs, kpi2rhs)\nkpi2lhs = IconText(kpi2icon, \"neutral\", \"m\", \"Active Users\", \"Monthly active users\", false, \"horizontal\")\nkpi2icon = Icon(\"users\", \"people\")\nkpi2rhs = BoldText(\"number\", \"47,320\", \"+6.8%\", \"metric\")\nkpi3 = SnippetCardItem(\"conversion-rate\", kpi3lhs, kpi3rhs)\nkpi3lhs = IconText(kpi3icon, \"neutral\", \"m\", \"Conversion Rate\", \"Avg. this month\", false, \"horizontal\")\nkpi3icon = Icon(\"trending-up\", \"charts\")\nkpi3rhs = BoldText(\"number\", \"5.7%\", \"+0.9%\", \"metric\")\nkpi4 = SnippetCardItem(\"churn-rate\", kpi4lhs, kpi4rhs)\nkpi4lhs = IconText(kpi4icon, \"neutral\", \"m\", \"Churn Rate\", \"Monthly customer churn\", false, \"horizontal\")\nkpi4icon = Icon(\"user-minus\", \"people\")\nkpi4rhs = BoldText(\"number\", \"2.1%\", \"-0.4%\", \"metric\")\nsections = SectionBlock([secUsers, secChannels], false)\nsecUsers = SectionItem(\"users\", \"User Activity\", [userTabs])\nuserTabs = Tabs([tabGrowth, tabRetention])\ntabGrowth = TabItem(\"growth\", \"User Growth\", [growthChart])\ngrowthChart = LineChart(months, [dauSeries, mauSeries], \"natural\", \"Month\", \"Users (k)\")\nmonths = [\"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\", \"Jan\", \"Feb\", \"Mar\", \"Apr\"]\ndauSeries = Series(\"DAU\", [5.1, 5.4, 5.8, 6.2, 6.7, 7.1, 7.6, 8.0, 8.4, 8.9, 9.3, 9.8])\nmauSeries = Series(\"MAU\", [28.4, 30.1, 31.8, 33.5, 35.2, 37.1, 38.8, 40.4, 42.1, 43.9, 45.6, 47.3])\ntabRetention = TabItem(\"retention\", \"Cohort Retention\", [retentionChart])\nretentionChart = LineChart(cohortMonths, [cohortJan, cohortFeb, cohortMar], \"natural\", \"Month Since Signup\", \"Retention (%)\")\ncohortMonths = [\"Month 0\", \"Month 1\", \"Month 2\", \"Month 3\", \"Month 4\", \"Month 5\", \"Month 6\"]\ncohortJan = Series(\"Jan Cohort\", [100, 80, 70, 62, 56, 51, 47])\ncohortFeb = Series(\"Feb Cohort\", [100, 83, 73, 65, 59, 54, 50])\ncohortMar = Series(\"Mar Cohort\", [100, 86, 76, 68, 62, 0, 0])\nsecChannels = SectionItem(\"channels\", \"Acquisition Channels\", [channelTabs])\nchannelTabs = Tabs([tabChannelBar, tabChannelTable])\ntabChannelBar = TabItem(\"channel-bar\", \"Revenue by Channel\", [channelBarChart])\nchannelBarChart = HorizontalBarChart(channels, [channelSeries], \"grouped\", \"Revenue ($k)\", \"Channel\")\nchannels = [\"Organic Search\", \"Paid Ads\", \"Referral Program\", \"Partner Network\", \"Direct / Brand\", \"Email Campaigns\"]\nchannelSeries = Series(\"Revenue ($k)\", [82.4, 68.1, 54.6, 38.2, 26.8, 14.4])\ntabChannelTable = TabItem(\"channel-table\", \"Full Breakdown\", [channelTable])\nchannelTable = Table([colChannel, colLeads, colConv, colCac, colRevenue, colStatus])\ncolChannel = Col(\"Channel\", channels, \"string\")\ncolLeads = Col(\"Leads\", [4210, 3180, 2050, 1120, 1290, 820], \"number\")\ncolConv = Col(\"Conv. Rate\", [\"6.4%\", \"5.1%\", \"9.4%\", \"11.8%\", \"5.8%\", \"8.1%\"], \"string\")\ncolCac = Col(\"CAC ($)\", [188, 395, 132, 109, 218, 154], \"number\")\ncolRevenue = Col(\"Revenue ($k)\", [82.4, 68.1, 54.6, 38.2, 26.8, 14.4], \"number\")\ncolStatus = Col(\"Status\", [\"Scaling\", \"Optimizing\", \"Scaling\", \"Growing\", \"Stable\", \"Stable\"], \"string\")\ndeepDiveHeader = InlineHeader(\"Support Health & Marketing ROI\", \"Click a card to explore deeper insights\")\ncompositeCards = CompositeCardBlock([cardSupport, cardMarketing], \"grid\", true, { type: \"continue_conversation\", context: \"Explore this deep-dive area in more detail\" })\ncardSupport = CompositeCardItem(\"support-health\", supportHeader, [supportMetric, supportChart, supportList])\nsupportHeader = IconText(supportIcon, \"neutral\", \"m\", \"Support Health\", \"Ticket volume & resolution trends\", false, \"horizontal\")\nsupportIcon = Icon(\"headphones\", \"communication\")\nsupportMetric = MetricIndicatorInline(\"94.2% CSAT\", \"Customer Satisfaction Score\", { direction: \"up\", value: 2.1 })\nsupportChart = BarChart([\"Jan\", \"Feb\", \"Mar\", \"Apr\"], [openSeries, resolvedSeries], \"grouped\", \"Month\", \"Tickets\")\nopenSeries = Series(\"Opened\", [341, 327, 308, 289])\nresolvedSeries = Series(\"Resolved\", [319, 318, 301, 283])\nsupportList = EntityList([sup1, sup2, sup3], \"small\")\nsup1 = { left: \"Avg. Resolution Time\", right: \"4h 12m\", rightVariant: \"text\" }\nsup2 = { left: \"First Contact Resolution\", right: \"78.4%\", rightVariant: \"number\" }\nsup3 = { left: \"Escalation Rate\", right: \"6.2%\", rightVariant: \"number\" }\ncardMarketing = CompositeCardItem(\"marketing-roi\", marketingHeader, [marketingMetric, marketingChart, marketingList])\nmarketingHeader = IconText(marketingIcon, \"neutral\", \"m\", \"Marketing ROI\", \"Spend vs. pipeline generated\", false, \"horizontal\")\nmarketingIcon = Icon(\"megaphone\", \"communication\")\nmarketingMetric = MetricIndicatorInline(\"3.8x Blended ROI\", \"Return on Ad Spend\", { direction: \"up\", value: 0.4 })\nmarketingChart = AreaChart([\"Jan\", \"Feb\", \"Mar\", \"Apr\"], [spendSeries, pipelineSeries], \"natural\", \"Month\", \"Value ($k)\")\nspendSeries = Series(\"Ad Spend\", [44.0, 46.8, 49.2, 51.6])\npipelineSeries = Series(\"Pipeline Generated\", [158.2, 171.5, 184.9, 196.1])\nmarketingList = EntityList([mkt1, mkt2, mkt3], \"small\")\nmkt1 = { left: \"Total Ad Spend (Apr)\", right: \"$51,600\", rightVariant: \"number\" }\nmkt2 = { left: \"Cost per Lead\", right: \"$48.20\", rightVariant: \"number\" }\nmkt3 = { left: \"Top Channel\", right: \"Organic Search\", rightVariant: \"text\" }\nactionButtons = Buttons([btnChannels, btnForecast], \"row\")\nbtnChannels = Button(\"Break Down by Channel\", { type: \"continue_conversation\", context: \"Break down this month's revenue by acquisition channel\" }, \"primary\")\nbtnForecast = Button(\"Forecast Next Quarter\", { type: \"continue_conversation\", context: \"Forecast revenue and active users for next quarter\" }, \"secondary\")","checkpoints":[{"at":510,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Analytics Dashboard","subtitle":"April 2026 · Business Performance Overview"},"partial":false,"hasDynamicProps":false,"statementId":"pageHeader"},{"type":"element","typeName":"SnippetCardBlock","props":{"items":[],"layout":"grid","responsive":true},"partial":false,"hasDynamicProps":false,"statementId":"kpiBlock"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["kpi1icon","kpi1rhs","kpi2","kpi3","kpi4","sections","deepDiveHeader","compositeCards","actionButtons"],"orphaned":[],"statementCount":5,"errors":[{"code":"null-required","component":"IconText","path":"/icon","message":"required field \"/icon\" cannot be null — signature: IconText(icon*: Icon, iconVariant*: \"neutral\"|\"info\"|\"success\"|\"warning\"|\"danger\"|\"inverted\"|\"filled\"|\"soft\", iconSize*: \"xs\"|\"s\"|\"m\"|\"l\"|\"xl\"|\"sm\"|\"md\"|\"lg\", title*: string, subtitle: string, bold*: boolean, layout*: \"horizontal\"|\"vertical\")","statementId":"kpi1lhs"},{"code":"null-required","component":"SnippetCardItem","path":"/lhs","message":"required field \"/lhs\" cannot be null — signature: SnippetCardItem(id: string, lhs*, rhs)","statementId":"kpi1"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":1020,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Analytics Dashboard","subtitle":"April 2026 · Business Performance Overview"},"partial":true,"hasDynamicProps":false,"statementId":"pageHeader"},{"type":"element","typeName":"SnippetCardBlock","props":{"items":[{"type":"element","typeName":"SnippetCardItem","props":{"id":"total-revenue","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"circle-dollar-sign","category":"finance"},"partial":true,"hasDynamicProps":false,"statementId":"kpi1icon"},"iconVariant":"neutral","iconSize":"m","title":"Total Revenue","subtitle":"Month to date","bold":false,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"kpi1lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"$284,500","subtext":"+12.4%","subtextVariant":"metric","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"kpi1rhs"}},"partial":true,"hasDynamicProps":false,"statementId":"kpi1"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"active-users","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"users","category":"people"},"partial":true,"hasDynamicProps":false,"statementId":"kpi2icon"},"iconVariant":"neutral","iconSize":"m","title":"Active Users","subtitle":"Monthly active users","bold":false,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"kpi2lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"47,320","subtext":"+6.8%","subtextVariant":"metric","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"kpi2rhs"}},"partial":true,"hasDynamicProps":false,"statementId":"kpi2"}],"layout":"grid","responsive":true},"partial":true,"hasDynamicProps":false,"statementId":"kpiBlock"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["kpi3icon","kpi3rhs","kpi4","sections","deepDiveHeader","compositeCards","actionButtons"],"orphaned":[],"statementCount":13,"errors":[{"code":"null-required","component":"IconText","path":"/icon","message":"required field \"/icon\" cannot be null — signature: IconText(icon*: Icon, iconVariant*: \"neutral\"|\"info\"|\"success\"|\"warning\"|\"danger\"|\"inverted\"|\"filled\"|\"soft\", iconSize*: \"xs\"|\"s\"|\"m\"|\"l\"|\"xl\"|\"sm\"|\"md\"|\"lg\", title*: string, subtitle: string, bold*: boolean, layout*: \"horizontal\"|\"vertical\")","statementId":"kpi3lhs"},{"code":"null-required","component":"SnippetCardItem","path":"/lhs","message":"required field \"/lhs\" cannot be null — signature: SnippetCardItem(id: string, lhs*, rhs)","statementId":"kpi3"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":1530,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Analytics Dashboard","subtitle":"April 2026 · Business Performance Overview"},"partial":true,"hasDynamicProps":false,"statementId":"pageHeader"},{"type":"element","typeName":"SnippetCardBlock","props":{"items":[{"type":"element","typeName":"SnippetCardItem","props":{"id":"total-revenue","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"circle-dollar-sign","category":"finance"},"partial":true,"hasDynamicProps":false,"statementId":"kpi1icon"},"iconVariant":"neutral","iconSize":"m","title":"Total Revenue","subtitle":"Month to date","bold":false,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"kpi1lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"$284,500","subtext":"+12.4%","subtextVariant":"metric","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"kpi1rhs"}},"partial":true,"hasDynamicProps":false,"statementId":"kpi1"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"active-users","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"users","category":"people"},"partial":true,"hasDynamicProps":false,"statementId":"kpi2icon"},"iconVariant":"neutral","iconSize":"m","title":"Active Users","subtitle":"Monthly active users","bold":false,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"kpi2lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"47,320","subtext":"+6.8%","subtextVariant":"metric","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"kpi2rhs"}},"partial":true,"hasDynamicProps":false,"statementId":"kpi2"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"conversion-rate","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"trending-up","category":"charts"},"partial":true,"hasDynamicProps":false,"statementId":"kpi3icon"},"iconVariant":"neutral","iconSize":"m","title":"Conversion Rate","subtitle":"Avg. this month","bold":false,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"kpi3lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"5.7%","subtext":"+0.9%","subtextVariant":"metric","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"kpi3rhs"}},"partial":true,"hasDynamicProps":false,"statementId":"kpi3"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"churn-rate","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"user-minus","category":"people"},"partial":true,"hasDynamicProps":false,"statementId":"kpi4icon"},"iconVariant":"neutral","iconSize":"m","title":"Churn Rate","subtitle":"Monthly customer churn","bold":false,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"kpi4lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"2.1%","subtext":"-0.4%","subtextVariant":"metric","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"kpi4rhs"}},"partial":true,"hasDynamicProps":false,"statementId":"kpi4"}],"layout":"grid","responsive":true},"partial":true,"hasDynamicProps":false,"statementId":"kpiBlock"},{"type":"element","typeName":"SectionBlock","props":{"sections":[{"type":"element","typeName":"SectionItem","props":{"value":"users","trigger":"User Activity","content":[{"type":"element","typeName":"Tabs","props":{"items":[]},"partial":true,"hasDynamicProps":false,"statementId":"userTabs"}]},"partial":true,"hasDynamicProps":false,"statementId":"secUsers"}],"isFoldable":false},"partial":true,"hasDynamicProps":false,"statementId":"sections"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["tabGrowth","t","secChannels","deepDiveHeader","compositeCards","actionButtons"],"orphaned":[],"statementCount":22,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":2040,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Analytics Dashboard","subtitle":"April 2026 · Business Performance Overview"},"partial":true,"hasDynamicProps":false,"statementId":"pageHeader"},{"type":"element","typeName":"SnippetCardBlock","props":{"items":[{"type":"element","typeName":"SnippetCardItem","props":{"id":"total-revenue","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"circle-dollar-sign","category":"finance"},"partial":true,"hasDynamicProps":false,"statementId":"kpi1icon"},"iconVariant":"neutral","iconSize":"m","title":"Total Revenue","subtitle":"Month to date","bold":false,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"kpi1lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"$284,500","subtext":"+12.4%","subtextVariant":"metric","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"kpi1rhs"}},"partial":true,"hasDynamicProps":false,"statementId":"kpi1"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"active-users","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"users","category":"people"},"partial":true,"hasDynamicProps":false,"statementId":"kpi2icon"},"iconVariant":"neutral","iconSize":"m","title":"Active Users","subtitle":"Monthly active users","bold":false,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"kpi2lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"47,320","subtext":"+6.8%","subtextVariant":"metric","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"kpi2rhs"}},"partial":true,"hasDynamicProps":false,"statementId":"kpi2"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"conversion-rate","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"trending-up","category":"charts"},"partial":true,"hasDynamicProps":false,"statementId":"kpi3icon"},"iconVariant":"neutral","iconSize":"m","title":"Conversion Rate","subtitle":"Avg. this month","bold":false,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"kpi3lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"5.7%","subtext":"+0.9%","subtextVariant":"metric","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"kpi3rhs"}},"partial":true,"hasDynamicProps":false,"statementId":"kpi3"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"churn-rate","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"user-minus","category":"people"},"partial":true,"hasDynamicProps":false,"statementId":"kpi4icon"},"iconVariant":"neutral","iconSize":"m","title":"Churn Rate","subtitle":"Monthly customer churn","bold":false,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"kpi4lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"2.1%","subtext":"-0.4%","subtextVariant":"metric","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"kpi4rhs"}},"partial":true,"hasDynamicProps":false,"statementId":"kpi4"}],"layout":"grid","responsive":true},"partial":true,"hasDynamicProps":false,"statementId":"kpiBlock"},{"type":"element","typeName":"SectionBlock","props":{"sections":[{"type":"element","typeName":"SectionItem","props":{"value":"users","trigger":"User Activity","content":[{"type":"element","typeName":"Tabs","props":{"items":[{"type":"element","typeName":"TabItem","props":{"value":"growth","trigger":"User Growth","content":[{"type":"element","typeName":"LineChart","props":{"labels":["May","Jun","Jul","Aug","Sep","Oct","Nov","Dec","Jan","Feb","Mar","Apr"],"series":[{"type":"element","typeName":"Series","props":{"category":"DAU","values":[5.1,5.4,5.8,6.2,6.7,7.1,7.6,8,8.4,8.9,9.3,9.8]},"partial":true,"hasDynamicProps":false,"statementId":"dauSeries"},{"type":"element","typeName":"Series","props":{"category":"MAU","values":[28.4,30.1,31.8,33.5,35.2,37.1,38.8,40.4,42.1,43.9,45.6,47.3]},"partial":true,"hasDynamicProps":false,"statementId":"mauSeries"}],"variant":"natural","xLabel":"Month","yLabel":"Users (k)"},"partial":true,"hasDynamicProps":false,"statementId":"growthChart"}]},"partial":true,"hasDynamicProps":false,"statementId":"tabGrowth"},{"type":"element","typeName":"TabItem","props":{"value":"retention","trigger":"Cohort Retention","content":[]},"partial":true,"hasDynamicProps":false,"statementId":"tabRetention"}]},"partial":true,"hasDynamicProps":false,"statementId":"userTabs"}]},"partial":true,"hasDynamicProps":false,"statementId":"secUsers"}],"isFoldable":false},"partial":true,"hasDynamicProps":false,"statementId":"sections"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["retentio","secChannels","deepDiveHeader","compositeCards","actionButtons"],"orphaned":[],"statementCount":28,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":2550,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Analytics Dashboard","subtitle":"April 2026 · Business Performance Overview"},"partial":false,"hasDynamicProps":false,"statementId":"pageHeader"},{"type":"element","typeName":"SnippetCardBlock","props":{"items":[{"type":"element","typeName":"SnippetCardItem","props":{"id":"total-revenue","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"circle-dollar-sign","category":"finance"},"partial":false,"hasDynamicProps":false,"statementId":"kpi1icon"},"iconVariant":"neutral","iconSize":"m","title":"Total Revenue","subtitle":"Month to date","bold":false,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"kpi1lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"$284,500","subtext":"+12.4%","subtextVariant":"metric","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"kpi1rhs"}},"partial":false,"hasDynamicProps":false,"statementId":"kpi1"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"active-users","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"users","category":"people"},"partial":false,"hasDynamicProps":false,"statementId":"kpi2icon"},"iconVariant":"neutral","iconSize":"m","title":"Active Users","subtitle":"Monthly active users","bold":false,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"kpi2lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"47,320","subtext":"+6.8%","subtextVariant":"metric","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"kpi2rhs"}},"partial":false,"hasDynamicProps":false,"statementId":"kpi2"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"conversion-rate","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"trending-up","category":"charts"},"partial":false,"hasDynamicProps":false,"statementId":"kpi3icon"},"iconVariant":"neutral","iconSize":"m","title":"Conversion Rate","subtitle":"Avg. this month","bold":false,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"kpi3lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"5.7%","subtext":"+0.9%","subtextVariant":"metric","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"kpi3rhs"}},"partial":false,"hasDynamicProps":false,"statementId":"kpi3"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"churn-rate","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"user-minus","category":"people"},"partial":false,"hasDynamicProps":false,"statementId":"kpi4icon"},"iconVariant":"neutral","iconSize":"m","title":"Churn Rate","subtitle":"Monthly customer churn","bold":false,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"kpi4lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"2.1%","subtext":"-0.4%","subtextVariant":"metric","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"kpi4rhs"}},"partial":false,"hasDynamicProps":false,"statementId":"kpi4"}],"layout":"grid","responsive":true},"partial":false,"hasDynamicProps":false,"statementId":"kpiBlock"},{"type":"element","typeName":"SectionBlock","props":{"sections":[{"type":"element","typeName":"SectionItem","props":{"value":"users","trigger":"User Activity","content":[{"type":"element","typeName":"Tabs","props":{"items":[{"type":"element","typeName":"TabItem","props":{"value":"growth","trigger":"User Growth","content":[{"type":"element","typeName":"LineChart","props":{"labels":["May","Jun","Jul","Aug","Sep","Oct","Nov","Dec","Jan","Feb","Mar","Apr"],"series":[{"type":"element","typeName":"Series","props":{"category":"DAU","values":[5.1,5.4,5.8,6.2,6.7,7.1,7.6,8,8.4,8.9,9.3,9.8]},"partial":false,"hasDynamicProps":false,"statementId":"dauSeries"},{"type":"element","typeName":"Series","props":{"category":"MAU","values":[28.4,30.1,31.8,33.5,35.2,37.1,38.8,40.4,42.1,43.9,45.6,47.3]},"partial":false,"hasDynamicProps":false,"statementId":"mauSeries"}],"variant":"natural","xLabel":"Month","yLabel":"Users (k)"},"partial":false,"hasDynamicProps":false,"statementId":"growthChart"}]},"partial":false,"hasDynamicProps":false,"statementId":"tabGrowth"},{"type":"element","typeName":"TabItem","props":{"value":"retention","trigger":"Cohort Retention","content":[{"type":"element","typeName":"LineChart","props":{"labels":["Month 0","Month 1","Month 2","Month 3","Month 4","Month 5","Month 6"],"series":[{"type":"element","typeName":"Series","props":{"category":"Jan Cohort","values":[100,80,70,62,56,51,47]},"partial":false,"hasDynamicProps":false,"statementId":"cohortJan"},{"type":"element","typeName":"Series","props":{"category":"Feb Cohort","values":[100,83,73,65,59,54,50]},"partial":false,"hasDynamicProps":false,"statementId":"cohortFeb"},{"type":"element","typeName":"Series","props":{"category":"Mar Cohort","values":[100,86,76,68,62,0,0]},"partial":false,"hasDynamicProps":false,"statementId":"cohortMar"}],"variant":"natural","xLabel":"Month Since Signup","yLabel":"Retention (%)"},"partial":false,"hasDynamicProps":false,"statementId":"retentionChart"}]},"partial":false,"hasDynamicProps":false,"statementId":"tabRetention"}]},"partial":false,"hasDynamicProps":false,"statementId":"userTabs"}]},"partial":false,"hasDynamicProps":false,"statementId":"secUsers"},{"type":"element","typeName":"SectionItem","props":{"value":"channels","trigger":"Acquisition Channels","content":[]},"partial":false,"hasDynamicProps":false,"statementId":"secChannels"}],"isFoldable":false},"partial":false,"hasDynamicProps":false,"statementId":"sections"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["T","deepDiveHeader","compositeCards","actionButtons"],"orphaned":[],"statementCount":35,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":3060,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Analytics Dashboard","subtitle":"April 2026 · Business Performance Overview"},"partial":false,"hasDynamicProps":false,"statementId":"pageHeader"},{"type":"element","typeName":"SnippetCardBlock","props":{"items":[{"type":"element","typeName":"SnippetCardItem","props":{"id":"total-revenue","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"circle-dollar-sign","category":"finance"},"partial":false,"hasDynamicProps":false,"statementId":"kpi1icon"},"iconVariant":"neutral","iconSize":"m","title":"Total Revenue","subtitle":"Month to date","bold":false,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"kpi1lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"$284,500","subtext":"+12.4%","subtextVariant":"metric","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"kpi1rhs"}},"partial":false,"hasDynamicProps":false,"statementId":"kpi1"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"active-users","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"users","category":"people"},"partial":false,"hasDynamicProps":false,"statementId":"kpi2icon"},"iconVariant":"neutral","iconSize":"m","title":"Active Users","subtitle":"Monthly active users","bold":false,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"kpi2lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"47,320","subtext":"+6.8%","subtextVariant":"metric","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"kpi2rhs"}},"partial":false,"hasDynamicProps":false,"statementId":"kpi2"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"conversion-rate","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"trending-up","category":"charts"},"partial":false,"hasDynamicProps":false,"statementId":"kpi3icon"},"iconVariant":"neutral","iconSize":"m","title":"Conversion Rate","subtitle":"Avg. this month","bold":false,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"kpi3lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"5.7%","subtext":"+0.9%","subtextVariant":"metric","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"kpi3rhs"}},"partial":false,"hasDynamicProps":false,"statementId":"kpi3"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"churn-rate","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"user-minus","category":"people"},"partial":false,"hasDynamicProps":false,"statementId":"kpi4icon"},"iconVariant":"neutral","iconSize":"m","title":"Churn Rate","subtitle":"Monthly customer churn","bold":false,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"kpi4lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"2.1%","subtext":"-0.4%","subtextVariant":"metric","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"kpi4rhs"}},"partial":false,"hasDynamicProps":false,"statementId":"kpi4"}],"layout":"grid","responsive":true},"partial":false,"hasDynamicProps":false,"statementId":"kpiBlock"},{"type":"element","typeName":"SectionBlock","props":{"sections":[{"type":"element","typeName":"SectionItem","props":{"value":"users","trigger":"User Activity","content":[{"type":"element","typeName":"Tabs","props":{"items":[{"type":"element","typeName":"TabItem","props":{"value":"growth","trigger":"User Growth","content":[{"type":"element","typeName":"LineChart","props":{"labels":["May","Jun","Jul","Aug","Sep","Oct","Nov","Dec","Jan","Feb","Mar","Apr"],"series":[{"type":"element","typeName":"Series","props":{"category":"DAU","values":[5.1,5.4,5.8,6.2,6.7,7.1,7.6,8,8.4,8.9,9.3,9.8]},"partial":false,"hasDynamicProps":false,"statementId":"dauSeries"},{"type":"element","typeName":"Series","props":{"category":"MAU","values":[28.4,30.1,31.8,33.5,35.2,37.1,38.8,40.4,42.1,43.9,45.6,47.3]},"partial":false,"hasDynamicProps":false,"statementId":"mauSeries"}],"variant":"natural","xLabel":"Month","yLabel":"Users (k)"},"partial":false,"hasDynamicProps":false,"statementId":"growthChart"}]},"partial":false,"hasDynamicProps":false,"statementId":"tabGrowth"},{"type":"element","typeName":"TabItem","props":{"value":"retention","trigger":"Cohort Retention","content":[{"type":"element","typeName":"LineChart","props":{"labels":["Month 0","Month 1","Month 2","Month 3","Month 4","Month 5","Month 6"],"series":[{"type":"element","typeName":"Series","props":{"category":"Jan Cohort","values":[100,80,70,62,56,51,47]},"partial":false,"hasDynamicProps":false,"statementId":"cohortJan"},{"type":"element","typeName":"Series","props":{"category":"Feb Cohort","values":[100,83,73,65,59,54,50]},"partial":false,"hasDynamicProps":false,"statementId":"cohortFeb"},{"type":"element","typeName":"Series","props":{"category":"Mar Cohort","values":[100,86,76,68,62,0,0]},"partial":false,"hasDynamicProps":false,"statementId":"cohortMar"}],"variant":"natural","xLabel":"Month Since Signup","yLabel":"Retention (%)"},"partial":false,"hasDynamicProps":false,"statementId":"retentionChart"}]},"partial":false,"hasDynamicProps":false,"statementId":"tabRetention"}]},"partial":false,"hasDynamicProps":false,"statementId":"userTabs"}]},"partial":false,"hasDynamicProps":false,"statementId":"secUsers"},{"type":"element","typeName":"SectionItem","props":{"value":"channels","trigger":"Acquisition Channels","content":[{"type":"element","typeName":"Tabs","props":{"items":[{"type":"element","typeName":"TabItem","props":{"value":"channel-bar","trigger":"Revenue by Channel","content":[{"type":"element","typeName":"HorizontalBarChart","props":{"labels":["Organic Search","Paid Ads","Referral Program","Partner Network","Direct / Brand","Email Campaigns"],"series":[{"type":"element","typeName":"Series","props":{"category":"Revenue ($k)","values":[82.4,68.1,54.6,38.2,26.8,14.4]},"partial":false,"hasDynamicProps":false,"statementId":"channelSeries"}],"variant":"grouped","xLabel":"Revenue ($k)","yLabel":"Channel"},"partial":false,"hasDynamicProps":false,"statementId":"channelBarChart"}]},"partial":false,"hasDynamicProps":false,"statementId":"tabChannelBar"},{"type":"element","typeName":"TabItem","props":{"value":"channel-table","trigger":"Full Breakdown","content":[]},"partial":false,"hasDynamicProps":false,"statementId":"tabChannelTable"}]},"partial":false,"hasDynamicProps":false,"statementId":"channelTabs"}]},"partial":false,"hasDynamicProps":false,"statementId":"secChannels"}],"isFoldable":false},"partial":false,"hasDynamicProps":false,"statementId":"sections"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["Tab","deepDiveHeader","compositeCards","actionButtons"],"orphaned":[],"statementCount":41,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":3570,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Analytics Dashboard","subtitle":"April 2026 · Business Performance Overview"},"partial":true,"hasDynamicProps":false,"statementId":"pageHeader"},{"type":"element","typeName":"SnippetCardBlock","props":{"items":[{"type":"element","typeName":"SnippetCardItem","props":{"id":"total-revenue","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"circle-dollar-sign","category":"finance"},"partial":true,"hasDynamicProps":false,"statementId":"kpi1icon"},"iconVariant":"neutral","iconSize":"m","title":"Total Revenue","subtitle":"Month to date","bold":false,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"kpi1lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"$284,500","subtext":"+12.4%","subtextVariant":"metric","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"kpi1rhs"}},"partial":true,"hasDynamicProps":false,"statementId":"kpi1"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"active-users","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"users","category":"people"},"partial":true,"hasDynamicProps":false,"statementId":"kpi2icon"},"iconVariant":"neutral","iconSize":"m","title":"Active Users","subtitle":"Monthly active users","bold":false,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"kpi2lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"47,320","subtext":"+6.8%","subtextVariant":"metric","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"kpi2rhs"}},"partial":true,"hasDynamicProps":false,"statementId":"kpi2"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"conversion-rate","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"trending-up","category":"charts"},"partial":true,"hasDynamicProps":false,"statementId":"kpi3icon"},"iconVariant":"neutral","iconSize":"m","title":"Conversion Rate","subtitle":"Avg. this month","bold":false,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"kpi3lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"5.7%","subtext":"+0.9%","subtextVariant":"metric","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"kpi3rhs"}},"partial":true,"hasDynamicProps":false,"statementId":"kpi3"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"churn-rate","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"user-minus","category":"people"},"partial":true,"hasDynamicProps":false,"statementId":"kpi4icon"},"iconVariant":"neutral","iconSize":"m","title":"Churn Rate","subtitle":"Monthly customer churn","bold":false,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"kpi4lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"2.1%","subtext":"-0.4%","subtextVariant":"metric","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"kpi4rhs"}},"partial":true,"hasDynamicProps":false,"statementId":"kpi4"}],"layout":"grid","responsive":true},"partial":true,"hasDynamicProps":false,"statementId":"kpiBlock"},{"type":"element","typeName":"SectionBlock","props":{"sections":[{"type":"element","typeName":"SectionItem","props":{"value":"users","trigger":"User Activity","content":[{"type":"element","typeName":"Tabs","props":{"items":[{"type":"element","typeName":"TabItem","props":{"value":"growth","trigger":"User Growth","content":[{"type":"element","typeName":"LineChart","props":{"labels":["May","Jun","Jul","Aug","Sep","Oct","Nov","Dec","Jan","Feb","Mar","Apr"],"series":[{"type":"element","typeName":"Series","props":{"category":"DAU","values":[5.1,5.4,5.8,6.2,6.7,7.1,7.6,8,8.4,8.9,9.3,9.8]},"partial":true,"hasDynamicProps":false,"statementId":"dauSeries"},{"type":"element","typeName":"Series","props":{"category":"MAU","values":[28.4,30.1,31.8,33.5,35.2,37.1,38.8,40.4,42.1,43.9,45.6,47.3]},"partial":true,"hasDynamicProps":false,"statementId":"mauSeries"}],"variant":"natural","xLabel":"Month","yLabel":"Users (k)"},"partial":true,"hasDynamicProps":false,"statementId":"growthChart"}]},"partial":true,"hasDynamicProps":false,"statementId":"tabGrowth"},{"type":"element","typeName":"TabItem","props":{"value":"retention","trigger":"Cohort Retention","content":[{"type":"element","typeName":"LineChart","props":{"labels":["Month 0","Month 1","Month 2","Month 3","Month 4","Month 5","Month 6"],"series":[{"type":"element","typeName":"Series","props":{"category":"Jan Cohort","values":[100,80,70,62,56,51,47]},"partial":true,"hasDynamicProps":false,"statementId":"cohortJan"},{"type":"element","typeName":"Series","props":{"category":"Feb Cohort","values":[100,83,73,65,59,54,50]},"partial":true,"hasDynamicProps":false,"statementId":"cohortFeb"},{"type":"element","typeName":"Series","props":{"category":"Mar Cohort","values":[100,86,76,68,62,0,0]},"partial":true,"hasDynamicProps":false,"statementId":"cohortMar"}],"variant":"natural","xLabel":"Month Since Signup","yLabel":"Retention (%)"},"partial":true,"hasDynamicProps":false,"statementId":"retentionChart"}]},"partial":true,"hasDynamicProps":false,"statementId":"tabRetention"}]},"partial":true,"hasDynamicProps":false,"statementId":"userTabs"}]},"partial":true,"hasDynamicProps":false,"statementId":"secUsers"},{"type":"element","typeName":"SectionItem","props":{"value":"channels","trigger":"Acquisition Channels","content":[{"type":"element","typeName":"Tabs","props":{"items":[{"type":"element","typeName":"TabItem","props":{"value":"channel-bar","trigger":"Revenue by Channel","content":[{"type":"element","typeName":"HorizontalBarChart","props":{"labels":["Organic Search","Paid Ads","Referral Program","Partner Network","Direct / Brand","Email Campaigns"],"series":[{"type":"element","typeName":"Series","props":{"category":"Revenue ($k)","values":[82.4,68.1,54.6,38.2,26.8,14.4]},"partial":true,"hasDynamicProps":false,"statementId":"channelSeries"}],"variant":"grouped","xLabel":"Revenue ($k)","yLabel":"Channel"},"partial":true,"hasDynamicProps":false,"statementId":"channelBarChart"}]},"partial":true,"hasDynamicProps":false,"statementId":"tabChannelBar"},{"type":"element","typeName":"TabItem","props":{"value":"channel-table","trigger":"Full Breakdown","content":[{"type":"element","typeName":"Table","props":{"columns":[{"type":"element","typeName":"Col","props":{"label":"Channel","data":["Organic Search","Paid Ads","Referral Program","Partner Network","Direct / Brand","Email Campaigns"],"type":"string"},"partial":true,"hasDynamicProps":false,"statementId":"colChannel"},{"type":"element","typeName":"Col","props":{"label":"Leads","data":[4210,3180,2050,1120,1290,820],"type":"number"},"partial":true,"hasDynamicProps":false,"statementId":"colLeads"},{"type":"element","typeName":"Col","props":{"label":"Conv. Rate","data":["6.4%","5.1%","9.4%","11.8%","5.8%","8.1%"],"type":"string"},"partial":true,"hasDynamicProps":false,"statementId":"colConv"},{"type":"element","typeName":"Col","props":{"label":"CAC ($)","data":[188,395,132,109,218,154],"type":"number"},"partial":true,"hasDynamicProps":false,"statementId":"colCac"},{"type":"element","typeName":"Col","props":{"label":"Revenue ($k)","data":[82.4,68.1,54.6,38.2,26.8,14.4],"type":"number"},"partial":true,"hasDynamicProps":false,"statementId":"colRevenue"},{"type":"element","typeName":"Col","props":{"label":"Status","data":["Scaling","Optimizing","Scaling","Growing","Stable","Sta"]},"partial":true,"hasDynamicProps":false,"statementId":"colStatus"}]},"partial":true,"hasDynamicProps":false,"statementId":"channelTable"}]},"partial":true,"hasDynamicProps":false,"statementId":"tabChannelTable"}]},"partial":true,"hasDynamicProps":false,"statementId":"channelTabs"}]},"partial":true,"hasDynamicProps":false,"statementId":"secChannels"}],"isFoldable":false},"partial":true,"hasDynamicProps":false,"statementId":"sections"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["deepDiveHeader","compositeCards","actionButtons"],"orphaned":[],"statementCount":47,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":4080,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Analytics Dashboard","subtitle":"April 2026 · Business Performance Overview"},"partial":true,"hasDynamicProps":false,"statementId":"pageHeader"},{"type":"element","typeName":"SnippetCardBlock","props":{"items":[{"type":"element","typeName":"SnippetCardItem","props":{"id":"total-revenue","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"circle-dollar-sign","category":"finance"},"partial":true,"hasDynamicProps":false,"statementId":"kpi1icon"},"iconVariant":"neutral","iconSize":"m","title":"Total Revenue","subtitle":"Month to date","bold":false,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"kpi1lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"$284,500","subtext":"+12.4%","subtextVariant":"metric","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"kpi1rhs"}},"partial":true,"hasDynamicProps":false,"statementId":"kpi1"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"active-users","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"users","category":"people"},"partial":true,"hasDynamicProps":false,"statementId":"kpi2icon"},"iconVariant":"neutral","iconSize":"m","title":"Active Users","subtitle":"Monthly active users","bold":false,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"kpi2lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"47,320","subtext":"+6.8%","subtextVariant":"metric","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"kpi2rhs"}},"partial":true,"hasDynamicProps":false,"statementId":"kpi2"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"conversion-rate","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"trending-up","category":"charts"},"partial":true,"hasDynamicProps":false,"statementId":"kpi3icon"},"iconVariant":"neutral","iconSize":"m","title":"Conversion Rate","subtitle":"Avg. this month","bold":false,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"kpi3lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"5.7%","subtext":"+0.9%","subtextVariant":"metric","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"kpi3rhs"}},"partial":true,"hasDynamicProps":false,"statementId":"kpi3"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"churn-rate","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"user-minus","category":"people"},"partial":true,"hasDynamicProps":false,"statementId":"kpi4icon"},"iconVariant":"neutral","iconSize":"m","title":"Churn Rate","subtitle":"Monthly customer churn","bold":false,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"kpi4lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"2.1%","subtext":"-0.4%","subtextVariant":"metric","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"kpi4rhs"}},"partial":true,"hasDynamicProps":false,"statementId":"kpi4"}],"layout":"grid","responsive":true},"partial":true,"hasDynamicProps":false,"statementId":"kpiBlock"},{"type":"element","typeName":"SectionBlock","props":{"sections":[{"type":"element","typeName":"SectionItem","props":{"value":"users","trigger":"User Activity","content":[{"type":"element","typeName":"Tabs","props":{"items":[{"type":"element","typeName":"TabItem","props":{"value":"growth","trigger":"User Growth","content":[{"type":"element","typeName":"LineChart","props":{"labels":["May","Jun","Jul","Aug","Sep","Oct","Nov","Dec","Jan","Feb","Mar","Apr"],"series":[{"type":"element","typeName":"Series","props":{"category":"DAU","values":[5.1,5.4,5.8,6.2,6.7,7.1,7.6,8,8.4,8.9,9.3,9.8]},"partial":true,"hasDynamicProps":false,"statementId":"dauSeries"},{"type":"element","typeName":"Series","props":{"category":"MAU","values":[28.4,30.1,31.8,33.5,35.2,37.1,38.8,40.4,42.1,43.9,45.6,47.3]},"partial":true,"hasDynamicProps":false,"statementId":"mauSeries"}],"variant":"natural","xLabel":"Month","yLabel":"Users (k)"},"partial":true,"hasDynamicProps":false,"statementId":"growthChart"}]},"partial":true,"hasDynamicProps":false,"statementId":"tabGrowth"},{"type":"element","typeName":"TabItem","props":{"value":"retention","trigger":"Cohort Retention","content":[{"type":"element","typeName":"LineChart","props":{"labels":["Month 0","Month 1","Month 2","Month 3","Month 4","Month 5","Month 6"],"series":[{"type":"element","typeName":"Series","props":{"category":"Jan Cohort","values":[100,80,70,62,56,51,47]},"partial":true,"hasDynamicProps":false,"statementId":"cohortJan"},{"type":"element","typeName":"Series","props":{"category":"Feb Cohort","values":[100,83,73,65,59,54,50]},"partial":true,"hasDynamicProps":false,"statementId":"cohortFeb"},{"type":"element","typeName":"Series","props":{"category":"Mar Cohort","values":[100,86,76,68,62,0,0]},"partial":true,"hasDynamicProps":false,"statementId":"cohortMar"}],"variant":"natural","xLabel":"Month Since Signup","yLabel":"Retention (%)"},"partial":true,"hasDynamicProps":false,"statementId":"retentionChart"}]},"partial":true,"hasDynamicProps":false,"statementId":"tabRetention"}]},"partial":true,"hasDynamicProps":false,"statementId":"userTabs"}]},"partial":true,"hasDynamicProps":false,"statementId":"secUsers"},{"type":"element","typeName":"SectionItem","props":{"value":"channels","trigger":"Acquisition Channels","content":[{"type":"element","typeName":"Tabs","props":{"items":[{"type":"element","typeName":"TabItem","props":{"value":"channel-bar","trigger":"Revenue by Channel","content":[{"type":"element","typeName":"HorizontalBarChart","props":{"labels":["Organic Search","Paid Ads","Referral Program","Partner Network","Direct / Brand","Email Campaigns"],"series":[{"type":"element","typeName":"Series","props":{"category":"Revenue ($k)","values":[82.4,68.1,54.6,38.2,26.8,14.4]},"partial":true,"hasDynamicProps":false,"statementId":"channelSeries"}],"variant":"grouped","xLabel":"Revenue ($k)","yLabel":"Channel"},"partial":true,"hasDynamicProps":false,"statementId":"channelBarChart"}]},"partial":true,"hasDynamicProps":false,"statementId":"tabChannelBar"},{"type":"element","typeName":"TabItem","props":{"value":"channel-table","trigger":"Full Breakdown","content":[{"type":"element","typeName":"Table","props":{"columns":[{"type":"element","typeName":"Col","props":{"label":"Channel","data":["Organic Search","Paid Ads","Referral Program","Partner Network","Direct / Brand","Email Campaigns"],"type":"string"},"partial":true,"hasDynamicProps":false,"statementId":"colChannel"},{"type":"element","typeName":"Col","props":{"label":"Leads","data":[4210,3180,2050,1120,1290,820],"type":"number"},"partial":true,"hasDynamicProps":false,"statementId":"colLeads"},{"type":"element","typeName":"Col","props":{"label":"Conv. Rate","data":["6.4%","5.1%","9.4%","11.8%","5.8%","8.1%"],"type":"string"},"partial":true,"hasDynamicProps":false,"statementId":"colConv"},{"type":"element","typeName":"Col","props":{"label":"CAC ($)","data":[188,395,132,109,218,154],"type":"number"},"partial":true,"hasDynamicProps":false,"statementId":"colCac"},{"type":"element","typeName":"Col","props":{"label":"Revenue ($k)","data":[82.4,68.1,54.6,38.2,26.8,14.4],"type":"number"},"partial":true,"hasDynamicProps":false,"statementId":"colRevenue"},{"type":"element","typeName":"Col","props":{"label":"Status","data":["Scaling","Optimizing","Scaling","Growing","Stable","Stable"],"type":"string"},"partial":true,"hasDynamicProps":false,"statementId":"colStatus"}]},"partial":true,"hasDynamicProps":false,"statementId":"channelTable"}]},"partial":true,"hasDynamicProps":false,"statementId":"tabChannelTable"}]},"partial":true,"hasDynamicProps":false,"statementId":"channelTabs"}]},"partial":true,"hasDynamicProps":false,"statementId":"secChannels"}],"isFoldable":false},"partial":true,"hasDynamicProps":false,"statementId":"sections"},{"type":"element","typeName":"InlineHeader","props":{"heading":"Support Health & Marketing ROI","description":"Click a card to explore deeper insights"},"partial":true,"hasDynamicProps":false,"statementId":"deepDiveHeader"},{"type":"element","typeName":"CompositeCardBlock","props":{"items":[{"type":"element","typeName":"CompositeCardItem","props":{"id":"support-health","header":null,"body":[]},"partial":true,"hasDynamicProps":false,"statementId":"cardSupport"}],"layout":"grid","responsive":true,"action":{"type":"continue_conversation","context":"Explore this deep-dive area in more detail"}},"partial":true,"hasDynamicProps":false,"statementId":"compositeCards"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["supportIcon","supportMetric","supportChart","supportList","cardMarketing","actionButtons"],"orphaned":[],"statementCount":51,"errors":[{"code":"null-required","component":"IconText","path":"/icon","message":"required field \"/icon\" cannot be null — signature: IconText(icon*: Icon, iconVariant*: \"neutral\"|\"info\"|\"success\"|\"warning\"|\"danger\"|\"inverted\"|\"filled\"|\"soft\", iconSize*: \"xs\"|\"s\"|\"m\"|\"l\"|\"xl\"|\"sm\"|\"md\"|\"lg\", title*: string, subtitle: string, bold*: boolean, layout*: \"horizontal\"|\"vertical\")","statementId":"supportHeader"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":4590,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Analytics Dashboard","subtitle":"April 2026 · Business Performance Overview"},"partial":true,"hasDynamicProps":false,"statementId":"pageHeader"},{"type":"element","typeName":"SnippetCardBlock","props":{"items":[{"type":"element","typeName":"SnippetCardItem","props":{"id":"total-revenue","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"circle-dollar-sign","category":"finance"},"partial":true,"hasDynamicProps":false,"statementId":"kpi1icon"},"iconVariant":"neutral","iconSize":"m","title":"Total Revenue","subtitle":"Month to date","bold":false,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"kpi1lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"$284,500","subtext":"+12.4%","subtextVariant":"metric","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"kpi1rhs"}},"partial":true,"hasDynamicProps":false,"statementId":"kpi1"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"active-users","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"users","category":"people"},"partial":true,"hasDynamicProps":false,"statementId":"kpi2icon"},"iconVariant":"neutral","iconSize":"m","title":"Active Users","subtitle":"Monthly active users","bold":false,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"kpi2lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"47,320","subtext":"+6.8%","subtextVariant":"metric","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"kpi2rhs"}},"partial":true,"hasDynamicProps":false,"statementId":"kpi2"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"conversion-rate","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"trending-up","category":"charts"},"partial":true,"hasDynamicProps":false,"statementId":"kpi3icon"},"iconVariant":"neutral","iconSize":"m","title":"Conversion Rate","subtitle":"Avg. this month","bold":false,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"kpi3lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"5.7%","subtext":"+0.9%","subtextVariant":"metric","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"kpi3rhs"}},"partial":true,"hasDynamicProps":false,"statementId":"kpi3"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"churn-rate","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"user-minus","category":"people"},"partial":true,"hasDynamicProps":false,"statementId":"kpi4icon"},"iconVariant":"neutral","iconSize":"m","title":"Churn Rate","subtitle":"Monthly customer churn","bold":false,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"kpi4lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"2.1%","subtext":"-0.4%","subtextVariant":"metric","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"kpi4rhs"}},"partial":true,"hasDynamicProps":false,"statementId":"kpi4"}],"layout":"grid","responsive":true},"partial":true,"hasDynamicProps":false,"statementId":"kpiBlock"},{"type":"element","typeName":"SectionBlock","props":{"sections":[{"type":"element","typeName":"SectionItem","props":{"value":"users","trigger":"User Activity","content":[{"type":"element","typeName":"Tabs","props":{"items":[{"type":"element","typeName":"TabItem","props":{"value":"growth","trigger":"User Growth","content":[{"type":"element","typeName":"LineChart","props":{"labels":["May","Jun","Jul","Aug","Sep","Oct","Nov","Dec","Jan","Feb","Mar","Apr"],"series":[{"type":"element","typeName":"Series","props":{"category":"DAU","values":[5.1,5.4,5.8,6.2,6.7,7.1,7.6,8,8.4,8.9,9.3,9.8]},"partial":true,"hasDynamicProps":false,"statementId":"dauSeries"},{"type":"element","typeName":"Series","props":{"category":"MAU","values":[28.4,30.1,31.8,33.5,35.2,37.1,38.8,40.4,42.1,43.9,45.6,47.3]},"partial":true,"hasDynamicProps":false,"statementId":"mauSeries"}],"variant":"natural","xLabel":"Month","yLabel":"Users (k)"},"partial":true,"hasDynamicProps":false,"statementId":"growthChart"}]},"partial":true,"hasDynamicProps":false,"statementId":"tabGrowth"},{"type":"element","typeName":"TabItem","props":{"value":"retention","trigger":"Cohort Retention","content":[{"type":"element","typeName":"LineChart","props":{"labels":["Month 0","Month 1","Month 2","Month 3","Month 4","Month 5","Month 6"],"series":[{"type":"element","typeName":"Series","props":{"category":"Jan Cohort","values":[100,80,70,62,56,51,47]},"partial":true,"hasDynamicProps":false,"statementId":"cohortJan"},{"type":"element","typeName":"Series","props":{"category":"Feb Cohort","values":[100,83,73,65,59,54,50]},"partial":true,"hasDynamicProps":false,"statementId":"cohortFeb"},{"type":"element","typeName":"Series","props":{"category":"Mar Cohort","values":[100,86,76,68,62,0,0]},"partial":true,"hasDynamicProps":false,"statementId":"cohortMar"}],"variant":"natural","xLabel":"Month Since Signup","yLabel":"Retention (%)"},"partial":true,"hasDynamicProps":false,"statementId":"retentionChart"}]},"partial":true,"hasDynamicProps":false,"statementId":"tabRetention"}]},"partial":true,"hasDynamicProps":false,"statementId":"userTabs"}]},"partial":true,"hasDynamicProps":false,"statementId":"secUsers"},{"type":"element","typeName":"SectionItem","props":{"value":"channels","trigger":"Acquisition Channels","content":[{"type":"element","typeName":"Tabs","props":{"items":[{"type":"element","typeName":"TabItem","props":{"value":"channel-bar","trigger":"Revenue by Channel","content":[{"type":"element","typeName":"HorizontalBarChart","props":{"labels":["Organic Search","Paid Ads","Referral Program","Partner Network","Direct / Brand","Email Campaigns"],"series":[{"type":"element","typeName":"Series","props":{"category":"Revenue ($k)","values":[82.4,68.1,54.6,38.2,26.8,14.4]},"partial":true,"hasDynamicProps":false,"statementId":"channelSeries"}],"variant":"grouped","xLabel":"Revenue ($k)","yLabel":"Channel"},"partial":true,"hasDynamicProps":false,"statementId":"channelBarChart"}]},"partial":true,"hasDynamicProps":false,"statementId":"tabChannelBar"},{"type":"element","typeName":"TabItem","props":{"value":"channel-table","trigger":"Full Breakdown","content":[{"type":"element","typeName":"Table","props":{"columns":[{"type":"element","typeName":"Col","props":{"label":"Channel","data":["Organic Search","Paid Ads","Referral Program","Partner Network","Direct / Brand","Email Campaigns"],"type":"string"},"partial":true,"hasDynamicProps":false,"statementId":"colChannel"},{"type":"element","typeName":"Col","props":{"label":"Leads","data":[4210,3180,2050,1120,1290,820],"type":"number"},"partial":true,"hasDynamicProps":false,"statementId":"colLeads"},{"type":"element","typeName":"Col","props":{"label":"Conv. Rate","data":["6.4%","5.1%","9.4%","11.8%","5.8%","8.1%"],"type":"string"},"partial":true,"hasDynamicProps":false,"statementId":"colConv"},{"type":"element","typeName":"Col","props":{"label":"CAC ($)","data":[188,395,132,109,218,154],"type":"number"},"partial":true,"hasDynamicProps":false,"statementId":"colCac"},{"type":"element","typeName":"Col","props":{"label":"Revenue ($k)","data":[82.4,68.1,54.6,38.2,26.8,14.4],"type":"number"},"partial":true,"hasDynamicProps":false,"statementId":"colRevenue"},{"type":"element","typeName":"Col","props":{"label":"Status","data":["Scaling","Optimizing","Scaling","Growing","Stable","Stable"],"type":"string"},"partial":true,"hasDynamicProps":false,"statementId":"colStatus"}]},"partial":true,"hasDynamicProps":false,"statementId":"channelTable"}]},"partial":true,"hasDynamicProps":false,"statementId":"tabChannelTable"}]},"partial":true,"hasDynamicProps":false,"statementId":"channelTabs"}]},"partial":true,"hasDynamicProps":false,"statementId":"secChannels"}],"isFoldable":false},"partial":true,"hasDynamicProps":false,"statementId":"sections"},{"type":"element","typeName":"InlineHeader","props":{"heading":"Support Health & Marketing ROI","description":"Click a card to explore deeper insights"},"partial":true,"hasDynamicProps":false,"statementId":"deepDiveHeader"},{"type":"element","typeName":"CompositeCardBlock","props":{"items":[{"type":"element","typeName":"CompositeCardItem","props":{"id":"support-health","header":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"headphones","category":"communication"},"partial":true,"hasDynamicProps":false,"statementId":"supportIcon"},"iconVariant":"neutral","iconSize":"m","title":"Support Health","subtitle":"Ticket volume & resolution trends","bold":false,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"supportHeader"},"body":[{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"94.2% CSAT","subtext":"Customer Satisfaction Score","trend":{"direction":"up","value":2.1}},"partial":true,"hasDynamicProps":false,"statementId":"supportMetric"},{"type":"element","typeName":"BarChart","props":{"labels":["Jan","Feb","Mar","Apr"],"series":[{"type":"element","typeName":"Series","props":{"category":"Opened","values":[341,327,308,289]},"partial":true,"hasDynamicProps":false,"statementId":"openSeries"},{"type":"element","typeName":"Series","props":{"category":"Resolved","values":[319,318,301,283]},"partial":true,"hasDynamicProps":false,"statementId":"resolvedSeries"}],"variant":"grouped","xLabel":"Month","yLabel":"Tickets"},"partial":true,"hasDynamicProps":false,"statementId":"supportChart"},{"type":"element","typeName":"EntityList","props":{"rows":[{"left":"Avg. Resolution Time","rig":null}],"size":"small"},"partial":true,"hasDynamicProps":false,"statementId":"supportList"}]},"partial":true,"hasDynamicProps":false,"statementId":"cardSupport"}],"layout":"grid","responsive":true,"action":{"type":"continue_conversation","context":"Explore this deep-dive area in more detail"}},"partial":true,"hasDynamicProps":false,"statementId":"compositeCards"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["sup2","sup3","cardMarketing","actionButtons"],"orphaned":[],"statementCount":58,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":5100,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Analytics Dashboard","subtitle":"April 2026 · Business Performance Overview"},"partial":false,"hasDynamicProps":false,"statementId":"pageHeader"},{"type":"element","typeName":"SnippetCardBlock","props":{"items":[{"type":"element","typeName":"SnippetCardItem","props":{"id":"total-revenue","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"circle-dollar-sign","category":"finance"},"partial":false,"hasDynamicProps":false,"statementId":"kpi1icon"},"iconVariant":"neutral","iconSize":"m","title":"Total Revenue","subtitle":"Month to date","bold":false,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"kpi1lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"$284,500","subtext":"+12.4%","subtextVariant":"metric","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"kpi1rhs"}},"partial":false,"hasDynamicProps":false,"statementId":"kpi1"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"active-users","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"users","category":"people"},"partial":false,"hasDynamicProps":false,"statementId":"kpi2icon"},"iconVariant":"neutral","iconSize":"m","title":"Active Users","subtitle":"Monthly active users","bold":false,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"kpi2lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"47,320","subtext":"+6.8%","subtextVariant":"metric","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"kpi2rhs"}},"partial":false,"hasDynamicProps":false,"statementId":"kpi2"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"conversion-rate","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"trending-up","category":"charts"},"partial":false,"hasDynamicProps":false,"statementId":"kpi3icon"},"iconVariant":"neutral","iconSize":"m","title":"Conversion Rate","subtitle":"Avg. this month","bold":false,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"kpi3lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"5.7%","subtext":"+0.9%","subtextVariant":"metric","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"kpi3rhs"}},"partial":false,"hasDynamicProps":false,"statementId":"kpi3"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"churn-rate","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"user-minus","category":"people"},"partial":false,"hasDynamicProps":false,"statementId":"kpi4icon"},"iconVariant":"neutral","iconSize":"m","title":"Churn Rate","subtitle":"Monthly customer churn","bold":false,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"kpi4lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"2.1%","subtext":"-0.4%","subtextVariant":"metric","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"kpi4rhs"}},"partial":false,"hasDynamicProps":false,"statementId":"kpi4"}],"layout":"grid","responsive":true},"partial":false,"hasDynamicProps":false,"statementId":"kpiBlock"},{"type":"element","typeName":"SectionBlock","props":{"sections":[{"type":"element","typeName":"SectionItem","props":{"value":"users","trigger":"User Activity","content":[{"type":"element","typeName":"Tabs","props":{"items":[{"type":"element","typeName":"TabItem","props":{"value":"growth","trigger":"User Growth","content":[{"type":"element","typeName":"LineChart","props":{"labels":["May","Jun","Jul","Aug","Sep","Oct","Nov","Dec","Jan","Feb","Mar","Apr"],"series":[{"type":"element","typeName":"Series","props":{"category":"DAU","values":[5.1,5.4,5.8,6.2,6.7,7.1,7.6,8,8.4,8.9,9.3,9.8]},"partial":false,"hasDynamicProps":false,"statementId":"dauSeries"},{"type":"element","typeName":"Series","props":{"category":"MAU","values":[28.4,30.1,31.8,33.5,35.2,37.1,38.8,40.4,42.1,43.9,45.6,47.3]},"partial":false,"hasDynamicProps":false,"statementId":"mauSeries"}],"variant":"natural","xLabel":"Month","yLabel":"Users (k)"},"partial":false,"hasDynamicProps":false,"statementId":"growthChart"}]},"partial":false,"hasDynamicProps":false,"statementId":"tabGrowth"},{"type":"element","typeName":"TabItem","props":{"value":"retention","trigger":"Cohort Retention","content":[{"type":"element","typeName":"LineChart","props":{"labels":["Month 0","Month 1","Month 2","Month 3","Month 4","Month 5","Month 6"],"series":[{"type":"element","typeName":"Series","props":{"category":"Jan Cohort","values":[100,80,70,62,56,51,47]},"partial":false,"hasDynamicProps":false,"statementId":"cohortJan"},{"type":"element","typeName":"Series","props":{"category":"Feb Cohort","values":[100,83,73,65,59,54,50]},"partial":false,"hasDynamicProps":false,"statementId":"cohortFeb"},{"type":"element","typeName":"Series","props":{"category":"Mar Cohort","values":[100,86,76,68,62,0,0]},"partial":false,"hasDynamicProps":false,"statementId":"cohortMar"}],"variant":"natural","xLabel":"Month Since Signup","yLabel":"Retention (%)"},"partial":false,"hasDynamicProps":false,"statementId":"retentionChart"}]},"partial":false,"hasDynamicProps":false,"statementId":"tabRetention"}]},"partial":false,"hasDynamicProps":false,"statementId":"userTabs"}]},"partial":false,"hasDynamicProps":false,"statementId":"secUsers"},{"type":"element","typeName":"SectionItem","props":{"value":"channels","trigger":"Acquisition Channels","content":[{"type":"element","typeName":"Tabs","props":{"items":[{"type":"element","typeName":"TabItem","props":{"value":"channel-bar","trigger":"Revenue by Channel","content":[{"type":"element","typeName":"HorizontalBarChart","props":{"labels":["Organic Search","Paid Ads","Referral Program","Partner Network","Direct / Brand","Email Campaigns"],"series":[{"type":"element","typeName":"Series","props":{"category":"Revenue ($k)","values":[82.4,68.1,54.6,38.2,26.8,14.4]},"partial":false,"hasDynamicProps":false,"statementId":"channelSeries"}],"variant":"grouped","xLabel":"Revenue ($k)","yLabel":"Channel"},"partial":false,"hasDynamicProps":false,"statementId":"channelBarChart"}]},"partial":false,"hasDynamicProps":false,"statementId":"tabChannelBar"},{"type":"element","typeName":"TabItem","props":{"value":"channel-table","trigger":"Full Breakdown","content":[{"type":"element","typeName":"Table","props":{"columns":[{"type":"element","typeName":"Col","props":{"label":"Channel","data":["Organic Search","Paid Ads","Referral Program","Partner Network","Direct / Brand","Email Campaigns"],"type":"string"},"partial":false,"hasDynamicProps":false,"statementId":"colChannel"},{"type":"element","typeName":"Col","props":{"label":"Leads","data":[4210,3180,2050,1120,1290,820],"type":"number"},"partial":false,"hasDynamicProps":false,"statementId":"colLeads"},{"type":"element","typeName":"Col","props":{"label":"Conv. Rate","data":["6.4%","5.1%","9.4%","11.8%","5.8%","8.1%"],"type":"string"},"partial":false,"hasDynamicProps":false,"statementId":"colConv"},{"type":"element","typeName":"Col","props":{"label":"CAC ($)","data":[188,395,132,109,218,154],"type":"number"},"partial":false,"hasDynamicProps":false,"statementId":"colCac"},{"type":"element","typeName":"Col","props":{"label":"Revenue ($k)","data":[82.4,68.1,54.6,38.2,26.8,14.4],"type":"number"},"partial":false,"hasDynamicProps":false,"statementId":"colRevenue"},{"type":"element","typeName":"Col","props":{"label":"Status","data":["Scaling","Optimizing","Scaling","Growing","Stable","Stable"],"type":"string"},"partial":false,"hasDynamicProps":false,"statementId":"colStatus"}]},"partial":false,"hasDynamicProps":false,"statementId":"channelTable"}]},"partial":false,"hasDynamicProps":false,"statementId":"tabChannelTable"}]},"partial":false,"hasDynamicProps":false,"statementId":"channelTabs"}]},"partial":false,"hasDynamicProps":false,"statementId":"secChannels"}],"isFoldable":false},"partial":false,"hasDynamicProps":false,"statementId":"sections"},{"type":"element","typeName":"InlineHeader","props":{"heading":"Support Health & Marketing ROI","description":"Click a card to explore deeper insights"},"partial":false,"hasDynamicProps":false,"statementId":"deepDiveHeader"},{"type":"element","typeName":"CompositeCardBlock","props":{"items":[{"type":"element","typeName":"CompositeCardItem","props":{"id":"support-health","header":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"headphones","category":"communication"},"partial":false,"hasDynamicProps":false,"statementId":"supportIcon"},"iconVariant":"neutral","iconSize":"m","title":"Support Health","subtitle":"Ticket volume & resolution trends","bold":false,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"supportHeader"},"body":[{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"94.2% CSAT","subtext":"Customer Satisfaction Score","trend":{"direction":"up","value":2.1}},"partial":false,"hasDynamicProps":false,"statementId":"supportMetric"},{"type":"element","typeName":"BarChart","props":{"labels":["Jan","Feb","Mar","Apr"],"series":[{"type":"element","typeName":"Series","props":{"category":"Opened","values":[341,327,308,289]},"partial":false,"hasDynamicProps":false,"statementId":"openSeries"},{"type":"element","typeName":"Series","props":{"category":"Resolved","values":[319,318,301,283]},"partial":false,"hasDynamicProps":false,"statementId":"resolvedSeries"}],"variant":"grouped","xLabel":"Month","yLabel":"Tickets"},"partial":false,"hasDynamicProps":false,"statementId":"supportChart"},{"type":"element","typeName":"EntityList","props":{"rows":[{"left":"Avg. Resolution Time","right":"4h 12m","rightVariant":"text"},{"left":"First Contact Resolution","right":"78.4%","rightVariant":"number"},{"left":"Escalation Rate","right":"6.2%","rightVariant":"number"}],"size":"small"},"partial":false,"hasDynamicProps":false,"statementId":"supportList"}]},"partial":false,"hasDynamicProps":false,"statementId":"cardSupport"},{"type":"element","typeName":"CompositeCardItem","props":{"id":"marketing-roi","header":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"megaphone","category":"communication"},"partial":false,"hasDynamicProps":false,"statementId":"marketingIcon"},"iconVariant":"neutral","iconSize":"m","title":"Marketing ROI","subtitle":"Spend vs. pipeline generated","bold":false,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"marketingHeader"},"body":[]},"partial":false,"hasDynamicProps":false,"statementId":"cardMarketing"}],"layout":"grid","responsive":true,"action":{"type":"continue_conversation","context":"Explore this deep-dive area in more detail"}},"partial":false,"hasDynamicProps":false,"statementId":"compositeCards"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["marketingMetric","marketingChart","marketingList","actionButtons"],"orphaned":[],"statementCount":63,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":5610,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Analytics Dashboard","subtitle":"April 2026 · Business Performance Overview"},"partial":true,"hasDynamicProps":false,"statementId":"pageHeader"},{"type":"element","typeName":"SnippetCardBlock","props":{"items":[{"type":"element","typeName":"SnippetCardItem","props":{"id":"total-revenue","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"circle-dollar-sign","category":"finance"},"partial":true,"hasDynamicProps":false,"statementId":"kpi1icon"},"iconVariant":"neutral","iconSize":"m","title":"Total Revenue","subtitle":"Month to date","bold":false,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"kpi1lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"$284,500","subtext":"+12.4%","subtextVariant":"metric","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"kpi1rhs"}},"partial":true,"hasDynamicProps":false,"statementId":"kpi1"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"active-users","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"users","category":"people"},"partial":true,"hasDynamicProps":false,"statementId":"kpi2icon"},"iconVariant":"neutral","iconSize":"m","title":"Active Users","subtitle":"Monthly active users","bold":false,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"kpi2lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"47,320","subtext":"+6.8%","subtextVariant":"metric","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"kpi2rhs"}},"partial":true,"hasDynamicProps":false,"statementId":"kpi2"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"conversion-rate","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"trending-up","category":"charts"},"partial":true,"hasDynamicProps":false,"statementId":"kpi3icon"},"iconVariant":"neutral","iconSize":"m","title":"Conversion Rate","subtitle":"Avg. this month","bold":false,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"kpi3lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"5.7%","subtext":"+0.9%","subtextVariant":"metric","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"kpi3rhs"}},"partial":true,"hasDynamicProps":false,"statementId":"kpi3"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"churn-rate","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"user-minus","category":"people"},"partial":true,"hasDynamicProps":false,"statementId":"kpi4icon"},"iconVariant":"neutral","iconSize":"m","title":"Churn Rate","subtitle":"Monthly customer churn","bold":false,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"kpi4lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"2.1%","subtext":"-0.4%","subtextVariant":"metric","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"kpi4rhs"}},"partial":true,"hasDynamicProps":false,"statementId":"kpi4"}],"layout":"grid","responsive":true},"partial":true,"hasDynamicProps":false,"statementId":"kpiBlock"},{"type":"element","typeName":"SectionBlock","props":{"sections":[{"type":"element","typeName":"SectionItem","props":{"value":"users","trigger":"User Activity","content":[{"type":"element","typeName":"Tabs","props":{"items":[{"type":"element","typeName":"TabItem","props":{"value":"growth","trigger":"User Growth","content":[{"type":"element","typeName":"LineChart","props":{"labels":["May","Jun","Jul","Aug","Sep","Oct","Nov","Dec","Jan","Feb","Mar","Apr"],"series":[{"type":"element","typeName":"Series","props":{"category":"DAU","values":[5.1,5.4,5.8,6.2,6.7,7.1,7.6,8,8.4,8.9,9.3,9.8]},"partial":true,"hasDynamicProps":false,"statementId":"dauSeries"},{"type":"element","typeName":"Series","props":{"category":"MAU","values":[28.4,30.1,31.8,33.5,35.2,37.1,38.8,40.4,42.1,43.9,45.6,47.3]},"partial":true,"hasDynamicProps":false,"statementId":"mauSeries"}],"variant":"natural","xLabel":"Month","yLabel":"Users (k)"},"partial":true,"hasDynamicProps":false,"statementId":"growthChart"}]},"partial":true,"hasDynamicProps":false,"statementId":"tabGrowth"},{"type":"element","typeName":"TabItem","props":{"value":"retention","trigger":"Cohort Retention","content":[{"type":"element","typeName":"LineChart","props":{"labels":["Month 0","Month 1","Month 2","Month 3","Month 4","Month 5","Month 6"],"series":[{"type":"element","typeName":"Series","props":{"category":"Jan Cohort","values":[100,80,70,62,56,51,47]},"partial":true,"hasDynamicProps":false,"statementId":"cohortJan"},{"type":"element","typeName":"Series","props":{"category":"Feb Cohort","values":[100,83,73,65,59,54,50]},"partial":true,"hasDynamicProps":false,"statementId":"cohortFeb"},{"type":"element","typeName":"Series","props":{"category":"Mar Cohort","values":[100,86,76,68,62,0,0]},"partial":true,"hasDynamicProps":false,"statementId":"cohortMar"}],"variant":"natural","xLabel":"Month Since Signup","yLabel":"Retention (%)"},"partial":true,"hasDynamicProps":false,"statementId":"retentionChart"}]},"partial":true,"hasDynamicProps":false,"statementId":"tabRetention"}]},"partial":true,"hasDynamicProps":false,"statementId":"userTabs"}]},"partial":true,"hasDynamicProps":false,"statementId":"secUsers"},{"type":"element","typeName":"SectionItem","props":{"value":"channels","trigger":"Acquisition Channels","content":[{"type":"element","typeName":"Tabs","props":{"items":[{"type":"element","typeName":"TabItem","props":{"value":"channel-bar","trigger":"Revenue by Channel","content":[{"type":"element","typeName":"HorizontalBarChart","props":{"labels":["Organic Search","Paid Ads","Referral Program","Partner Network","Direct / Brand","Email Campaigns"],"series":[{"type":"element","typeName":"Series","props":{"category":"Revenue ($k)","values":[82.4,68.1,54.6,38.2,26.8,14.4]},"partial":true,"hasDynamicProps":false,"statementId":"channelSeries"}],"variant":"grouped","xLabel":"Revenue ($k)","yLabel":"Channel"},"partial":true,"hasDynamicProps":false,"statementId":"channelBarChart"}]},"partial":true,"hasDynamicProps":false,"statementId":"tabChannelBar"},{"type":"element","typeName":"TabItem","props":{"value":"channel-table","trigger":"Full Breakdown","content":[{"type":"element","typeName":"Table","props":{"columns":[{"type":"element","typeName":"Col","props":{"label":"Channel","data":["Organic Search","Paid Ads","Referral Program","Partner Network","Direct / Brand","Email Campaigns"],"type":"string"},"partial":true,"hasDynamicProps":false,"statementId":"colChannel"},{"type":"element","typeName":"Col","props":{"label":"Leads","data":[4210,3180,2050,1120,1290,820],"type":"number"},"partial":true,"hasDynamicProps":false,"statementId":"colLeads"},{"type":"element","typeName":"Col","props":{"label":"Conv. Rate","data":["6.4%","5.1%","9.4%","11.8%","5.8%","8.1%"],"type":"string"},"partial":true,"hasDynamicProps":false,"statementId":"colConv"},{"type":"element","typeName":"Col","props":{"label":"CAC ($)","data":[188,395,132,109,218,154],"type":"number"},"partial":true,"hasDynamicProps":false,"statementId":"colCac"},{"type":"element","typeName":"Col","props":{"label":"Revenue ($k)","data":[82.4,68.1,54.6,38.2,26.8,14.4],"type":"number"},"partial":true,"hasDynamicProps":false,"statementId":"colRevenue"},{"type":"element","typeName":"Col","props":{"label":"Status","data":["Scaling","Optimizing","Scaling","Growing","Stable","Stable"],"type":"string"},"partial":true,"hasDynamicProps":false,"statementId":"colStatus"}]},"partial":true,"hasDynamicProps":false,"statementId":"channelTable"}]},"partial":true,"hasDynamicProps":false,"statementId":"tabChannelTable"}]},"partial":true,"hasDynamicProps":false,"statementId":"channelTabs"}]},"partial":true,"hasDynamicProps":false,"statementId":"secChannels"}],"isFoldable":false},"partial":true,"hasDynamicProps":false,"statementId":"sections"},{"type":"element","typeName":"InlineHeader","props":{"heading":"Support Health & Marketing ROI","description":"Click a card to explore deeper insights"},"partial":true,"hasDynamicProps":false,"statementId":"deepDiveHeader"},{"type":"element","typeName":"CompositeCardBlock","props":{"items":[{"type":"element","typeName":"CompositeCardItem","props":{"id":"support-health","header":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"headphones","category":"communication"},"partial":true,"hasDynamicProps":false,"statementId":"supportIcon"},"iconVariant":"neutral","iconSize":"m","title":"Support Health","subtitle":"Ticket volume & resolution trends","bold":false,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"supportHeader"},"body":[{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"94.2% CSAT","subtext":"Customer Satisfaction Score","trend":{"direction":"up","value":2.1}},"partial":true,"hasDynamicProps":false,"statementId":"supportMetric"},{"type":"element","typeName":"BarChart","props":{"labels":["Jan","Feb","Mar","Apr"],"series":[{"type":"element","typeName":"Series","props":{"category":"Opened","values":[341,327,308,289]},"partial":true,"hasDynamicProps":false,"statementId":"openSeries"},{"type":"element","typeName":"Series","props":{"category":"Resolved","values":[319,318,301,283]},"partial":true,"hasDynamicProps":false,"statementId":"resolvedSeries"}],"variant":"grouped","xLabel":"Month","yLabel":"Tickets"},"partial":true,"hasDynamicProps":false,"statementId":"supportChart"},{"type":"element","typeName":"EntityList","props":{"rows":[{"left":"Avg. Resolution Time","right":"4h 12m","rightVariant":"text"},{"left":"First Contact Resolution","right":"78.4%","rightVariant":"number"},{"left":"Escalation Rate","right":"6.2%","rightVariant":"number"}],"size":"small"},"partial":true,"hasDynamicProps":false,"statementId":"supportList"}]},"partial":true,"hasDynamicProps":false,"statementId":"cardSupport"},{"type":"element","typeName":"CompositeCardItem","props":{"id":"marketing-roi","header":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"megaphone","category":"communication"},"partial":true,"hasDynamicProps":false,"statementId":"marketingIcon"},"iconVariant":"neutral","iconSize":"m","title":"Marketing ROI","subtitle":"Spend vs. pipeline generated","bold":false,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"marketingHeader"},"body":[{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"3.8x Blended ROI","subtext":"Return on Ad Spend","trend":{"direction":"up","value":0.4}},"partial":true,"hasDynamicProps":false,"statementId":"marketingMetric"},{"type":"element","typeName":"AreaChart","props":{"labels":["Jan","Feb","Mar","Apr"],"series":[{"type":"element","typeName":"Series","props":{"category":"Ad Spend","values":[44,46.8,49.2,51.6]},"partial":true,"hasDynamicProps":false,"statementId":"spendSeries"},{"type":"element","typeName":"Series","props":{"category":"Pipeline Generated","values":[158.2,171.5,184.9,196.1]},"partial":true,"hasDynamicProps":false,"statementId":"pipelineSeries"}],"variant":"natural","xLabel":"Month","yLabel":"Value ($k)"},"partial":true,"hasDynamicProps":false,"statementId":"marketingChart"},{"type":"element","typeName":"EntityList","props":{"rows":[{"left":"Total Ad Spend (Apr)","right":"$51,600","rightVariant":"number"},{"left":"Co"}],"size":"small"},"partial":true,"hasDynamicProps":false,"statementId":"marketingList"}]},"partial":true,"hasDynamicProps":false,"statementId":"cardMarketing"}],"layout":"grid","responsive":true,"action":{"type":"continue_conversation","context":"Explore this deep-dive area in more detail"}},"partial":true,"hasDynamicProps":false,"statementId":"compositeCards"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["mkt3","actionButtons"],"orphaned":[],"statementCount":70,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":6118,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Analytics Dashboard","subtitle":"April 2026 · Business Performance Overview"},"partial":false,"hasDynamicProps":false,"statementId":"pageHeader"},{"type":"element","typeName":"SnippetCardBlock","props":{"items":[{"type":"element","typeName":"SnippetCardItem","props":{"id":"total-revenue","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"circle-dollar-sign","category":"finance"},"partial":false,"hasDynamicProps":false,"statementId":"kpi1icon"},"iconVariant":"neutral","iconSize":"m","title":"Total Revenue","subtitle":"Month to date","bold":false,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"kpi1lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"$284,500","subtext":"+12.4%","subtextVariant":"metric","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"kpi1rhs"}},"partial":false,"hasDynamicProps":false,"statementId":"kpi1"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"active-users","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"users","category":"people"},"partial":false,"hasDynamicProps":false,"statementId":"kpi2icon"},"iconVariant":"neutral","iconSize":"m","title":"Active Users","subtitle":"Monthly active users","bold":false,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"kpi2lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"47,320","subtext":"+6.8%","subtextVariant":"metric","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"kpi2rhs"}},"partial":false,"hasDynamicProps":false,"statementId":"kpi2"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"conversion-rate","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"trending-up","category":"charts"},"partial":false,"hasDynamicProps":false,"statementId":"kpi3icon"},"iconVariant":"neutral","iconSize":"m","title":"Conversion Rate","subtitle":"Avg. this month","bold":false,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"kpi3lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"5.7%","subtext":"+0.9%","subtextVariant":"metric","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"kpi3rhs"}},"partial":false,"hasDynamicProps":false,"statementId":"kpi3"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"churn-rate","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"user-minus","category":"people"},"partial":false,"hasDynamicProps":false,"statementId":"kpi4icon"},"iconVariant":"neutral","iconSize":"m","title":"Churn Rate","subtitle":"Monthly customer churn","bold":false,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"kpi4lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"2.1%","subtext":"-0.4%","subtextVariant":"metric","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"kpi4rhs"}},"partial":false,"hasDynamicProps":false,"statementId":"kpi4"}],"layout":"grid","responsive":true},"partial":false,"hasDynamicProps":false,"statementId":"kpiBlock"},{"type":"element","typeName":"SectionBlock","props":{"sections":[{"type":"element","typeName":"SectionItem","props":{"value":"users","trigger":"User Activity","content":[{"type":"element","typeName":"Tabs","props":{"items":[{"type":"element","typeName":"TabItem","props":{"value":"growth","trigger":"User Growth","content":[{"type":"element","typeName":"LineChart","props":{"labels":["May","Jun","Jul","Aug","Sep","Oct","Nov","Dec","Jan","Feb","Mar","Apr"],"series":[{"type":"element","typeName":"Series","props":{"category":"DAU","values":[5.1,5.4,5.8,6.2,6.7,7.1,7.6,8,8.4,8.9,9.3,9.8]},"partial":false,"hasDynamicProps":false,"statementId":"dauSeries"},{"type":"element","typeName":"Series","props":{"category":"MAU","values":[28.4,30.1,31.8,33.5,35.2,37.1,38.8,40.4,42.1,43.9,45.6,47.3]},"partial":false,"hasDynamicProps":false,"statementId":"mauSeries"}],"variant":"natural","xLabel":"Month","yLabel":"Users (k)"},"partial":false,"hasDynamicProps":false,"statementId":"growthChart"}]},"partial":false,"hasDynamicProps":false,"statementId":"tabGrowth"},{"type":"element","typeName":"TabItem","props":{"value":"retention","trigger":"Cohort Retention","content":[{"type":"element","typeName":"LineChart","props":{"labels":["Month 0","Month 1","Month 2","Month 3","Month 4","Month 5","Month 6"],"series":[{"type":"element","typeName":"Series","props":{"category":"Jan Cohort","values":[100,80,70,62,56,51,47]},"partial":false,"hasDynamicProps":false,"statementId":"cohortJan"},{"type":"element","typeName":"Series","props":{"category":"Feb Cohort","values":[100,83,73,65,59,54,50]},"partial":false,"hasDynamicProps":false,"statementId":"cohortFeb"},{"type":"element","typeName":"Series","props":{"category":"Mar Cohort","values":[100,86,76,68,62,0,0]},"partial":false,"hasDynamicProps":false,"statementId":"cohortMar"}],"variant":"natural","xLabel":"Month Since Signup","yLabel":"Retention (%)"},"partial":false,"hasDynamicProps":false,"statementId":"retentionChart"}]},"partial":false,"hasDynamicProps":false,"statementId":"tabRetention"}]},"partial":false,"hasDynamicProps":false,"statementId":"userTabs"}]},"partial":false,"hasDynamicProps":false,"statementId":"secUsers"},{"type":"element","typeName":"SectionItem","props":{"value":"channels","trigger":"Acquisition Channels","content":[{"type":"element","typeName":"Tabs","props":{"items":[{"type":"element","typeName":"TabItem","props":{"value":"channel-bar","trigger":"Revenue by Channel","content":[{"type":"element","typeName":"HorizontalBarChart","props":{"labels":["Organic Search","Paid Ads","Referral Program","Partner Network","Direct / Brand","Email Campaigns"],"series":[{"type":"element","typeName":"Series","props":{"category":"Revenue ($k)","values":[82.4,68.1,54.6,38.2,26.8,14.4]},"partial":false,"hasDynamicProps":false,"statementId":"channelSeries"}],"variant":"grouped","xLabel":"Revenue ($k)","yLabel":"Channel"},"partial":false,"hasDynamicProps":false,"statementId":"channelBarChart"}]},"partial":false,"hasDynamicProps":false,"statementId":"tabChannelBar"},{"type":"element","typeName":"TabItem","props":{"value":"channel-table","trigger":"Full Breakdown","content":[{"type":"element","typeName":"Table","props":{"columns":[{"type":"element","typeName":"Col","props":{"label":"Channel","data":["Organic Search","Paid Ads","Referral Program","Partner Network","Direct / Brand","Email Campaigns"],"type":"string"},"partial":false,"hasDynamicProps":false,"statementId":"colChannel"},{"type":"element","typeName":"Col","props":{"label":"Leads","data":[4210,3180,2050,1120,1290,820],"type":"number"},"partial":false,"hasDynamicProps":false,"statementId":"colLeads"},{"type":"element","typeName":"Col","props":{"label":"Conv. Rate","data":["6.4%","5.1%","9.4%","11.8%","5.8%","8.1%"],"type":"string"},"partial":false,"hasDynamicProps":false,"statementId":"colConv"},{"type":"element","typeName":"Col","props":{"label":"CAC ($)","data":[188,395,132,109,218,154],"type":"number"},"partial":false,"hasDynamicProps":false,"statementId":"colCac"},{"type":"element","typeName":"Col","props":{"label":"Revenue ($k)","data":[82.4,68.1,54.6,38.2,26.8,14.4],"type":"number"},"partial":false,"hasDynamicProps":false,"statementId":"colRevenue"},{"type":"element","typeName":"Col","props":{"label":"Status","data":["Scaling","Optimizing","Scaling","Growing","Stable","Stable"],"type":"string"},"partial":false,"hasDynamicProps":false,"statementId":"colStatus"}]},"partial":false,"hasDynamicProps":false,"statementId":"channelTable"}]},"partial":false,"hasDynamicProps":false,"statementId":"tabChannelTable"}]},"partial":false,"hasDynamicProps":false,"statementId":"channelTabs"}]},"partial":false,"hasDynamicProps":false,"statementId":"secChannels"}],"isFoldable":false},"partial":false,"hasDynamicProps":false,"statementId":"sections"},{"type":"element","typeName":"InlineHeader","props":{"heading":"Support Health & Marketing ROI","description":"Click a card to explore deeper insights"},"partial":false,"hasDynamicProps":false,"statementId":"deepDiveHeader"},{"type":"element","typeName":"CompositeCardBlock","props":{"items":[{"type":"element","typeName":"CompositeCardItem","props":{"id":"support-health","header":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"headphones","category":"communication"},"partial":false,"hasDynamicProps":false,"statementId":"supportIcon"},"iconVariant":"neutral","iconSize":"m","title":"Support Health","subtitle":"Ticket volume & resolution trends","bold":false,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"supportHeader"},"body":[{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"94.2% CSAT","subtext":"Customer Satisfaction Score","trend":{"direction":"up","value":2.1}},"partial":false,"hasDynamicProps":false,"statementId":"supportMetric"},{"type":"element","typeName":"BarChart","props":{"labels":["Jan","Feb","Mar","Apr"],"series":[{"type":"element","typeName":"Series","props":{"category":"Opened","values":[341,327,308,289]},"partial":false,"hasDynamicProps":false,"statementId":"openSeries"},{"type":"element","typeName":"Series","props":{"category":"Resolved","values":[319,318,301,283]},"partial":false,"hasDynamicProps":false,"statementId":"resolvedSeries"}],"variant":"grouped","xLabel":"Month","yLabel":"Tickets"},"partial":false,"hasDynamicProps":false,"statementId":"supportChart"},{"type":"element","typeName":"EntityList","props":{"rows":[{"left":"Avg. Resolution Time","right":"4h 12m","rightVariant":"text"},{"left":"First Contact Resolution","right":"78.4%","rightVariant":"number"},{"left":"Escalation Rate","right":"6.2%","rightVariant":"number"}],"size":"small"},"partial":false,"hasDynamicProps":false,"statementId":"supportList"}]},"partial":false,"hasDynamicProps":false,"statementId":"cardSupport"},{"type":"element","typeName":"CompositeCardItem","props":{"id":"marketing-roi","header":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"megaphone","category":"communication"},"partial":false,"hasDynamicProps":false,"statementId":"marketingIcon"},"iconVariant":"neutral","iconSize":"m","title":"Marketing ROI","subtitle":"Spend vs. pipeline generated","bold":false,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"marketingHeader"},"body":[{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"3.8x Blended ROI","subtext":"Return on Ad Spend","trend":{"direction":"up","value":0.4}},"partial":false,"hasDynamicProps":false,"statementId":"marketingMetric"},{"type":"element","typeName":"AreaChart","props":{"labels":["Jan","Feb","Mar","Apr"],"series":[{"type":"element","typeName":"Series","props":{"category":"Ad Spend","values":[44,46.8,49.2,51.6]},"partial":false,"hasDynamicProps":false,"statementId":"spendSeries"},{"type":"element","typeName":"Series","props":{"category":"Pipeline Generated","values":[158.2,171.5,184.9,196.1]},"partial":false,"hasDynamicProps":false,"statementId":"pipelineSeries"}],"variant":"natural","xLabel":"Month","yLabel":"Value ($k)"},"partial":false,"hasDynamicProps":false,"statementId":"marketingChart"},{"type":"element","typeName":"EntityList","props":{"rows":[{"left":"Total Ad Spend (Apr)","right":"$51,600","rightVariant":"number"},{"left":"Cost per Lead","right":"$48.20","rightVariant":"number"},{"left":"Top Channel","right":"Organic Search","rightVariant":"text"}],"size":"small"},"partial":false,"hasDynamicProps":false,"statementId":"marketingList"}]},"partial":false,"hasDynamicProps":false,"statementId":"cardMarketing"}],"layout":"grid","responsive":true,"action":{"type":"continue_conversation","context":"Explore this deep-dive area in more detail"}},"partial":false,"hasDynamicProps":false,"statementId":"compositeCards"},{"type":"element","typeName":"Buttons","props":{"buttons":[{"type":"element","typeName":"Button","props":{"label":"Break Down by Channel","action":{"type":"continue_conversation","context":"Break down this month's revenue by acquisition channel"},"variant":"primary"},"partial":false,"hasDynamicProps":false,"statementId":"btnChannels"},{"type":"element","typeName":"Button","props":{"label":"Forecast Next Quarter","action":{"type":"continue_conversation","context":"Forecast revenue and active users for next quarter"},"variant":"secondary"},"partial":false,"hasDynamicProps":false,"statementId":"btnForecast"}],"direction":"row"},"partial":false,"hasDynamicProps":false,"statementId":"actionButtons"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":74,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"chat-example-3","schema":"chat","input":"Example 3 — Form response (help-me-choose/plan asks: inspiration cards FIRST, then a compact form). The inspiration-card images below came from an image tool call — include such cards ONLY when you have real URLs; with no real URLs, start directly with the form:\n\nroot = Card([header, intro, inspoCards, form])\nheader = CardHeader(\"Plan Your Trip\", \"A few ideas to spark inspiration — then tell me your preferences\")\nintro = TextContent(\"Tap a style to jump straight in, or fill the quick form below for a tailored plan.\")\ninspoCards = VisualCardBlock([insp1, insp2], \"grid\", true, { type: \"continue_conversation\", context: \"Plan me a trip in this style with destinations, itinerary, and costs\" })\ninsp1 = VisualCardItem(insp1body, \"inspo-beach\", \"https://www.travelandleisure.com/thmb/cuAB7XMQ6s_Gji-no956KNopT7M=/1500x0/filters:no_upscale():max_bytes(150000):strip_icc()/TAL-maldives-TROPVACAY0325-1b5047d5931d4850b47d082966f9f563.jpg\", insp1tag, \"Maldives overwater villas\")\ninsp1body = BoldText(\"text\", \"Tropical Beach Escape\", \"Overwater villas & snorkeling\")\ninsp1tag = Tag(\"Relaxation\", insp1icon, \"sm\", \"info\")\ninsp1icon = Icon(\"umbrella\", \"travel\")\ninsp2 = VisualCardItem(insp2body, \"inspo-mountain\", \"https://www.salkantaytrekmachu.com/img/adventure-travel-peru-032.jpg\", insp2tag, \"Trekking in the Peruvian Andes\")\ninsp2body = BoldText(\"text\", \"Mountain Adventure\", \"Trekking & epic views\")\ninsp2tag = Tag(\"Active\", insp2icon, \"sm\", \"warning\")\ninsp2icon = Icon(\"mountain\", \"nature\")\nform = Form(\"trip-planner\", formButtons, [fcDestination, fcDates, fcTravellers, fcTripType, fcBudget, fcStay, fcNotes])\nformButtons = Buttons([btnSubmit, btnSkip], \"row\")\nbtnSubmit = Button(\"Plan My Trip\", { type: \"continue_conversation\", context: \"Help me plan a trip based on my form submission\" }, \"primary\")\nbtnSkip = Button(\"Just Surprise Me\", { type: \"continue_conversation\", context: \"Skip the form and surprise me with a destination and itinerary\" }, \"secondary\")\nfcDestination = FormControl(\"Destination\", destinationInput, \"Leave blank if you'd like suggestions\")\ndestinationInput = Input(\"destination\", \"e.g. Tokyo, Japan — or leave blank\", \"text\")\nfcDates = FormControl(\"Travel Dates\", datesPicker, \"Select your start and end dates\")\ndatesPicker = DatePicker(\"travel-dates\", \"range\", { required: true })\nfcTravellers = FormControl(\"Number of Travellers\", travellersSelect, \"How many people are travelling?\")\ntravellersSelect = Select(\"num-travellers\", [tr1, tr2, tr3, tr4], \"Select number of travellers\", { required: true })\ntr1 = SelectItem(\"1\", \"1 — Solo\")\ntr2 = SelectItem(\"2\", \"2 — Couple\")\ntr3 = SelectItem(\"3-4\", \"3–4 People\")\ntr4 = SelectItem(\"5+\", \"5+ People\")\nfcTripType = FormControl(\"Trip Type\", tripTypeCards, \"What kind of trip are you planning?\")\ntripTypeCards = OptionCards(\"trip-type\", \"single\", [oc1, oc2, oc3, oc4], { required: true })\noc1 = OptionCard(\"adventure\", \"Adventure\", \"Hiking, trekking & outdoor activities\", oc1icon)\noc1icon = Icon(\"mountain\", \"nature\")\noc2 = OptionCard(\"beach\", \"Beach & Relaxation\", \"Sun, sand & leisure\", oc2icon)\noc2icon = Icon(\"waves\", \"nature\")\noc3 = OptionCard(\"cultural\", \"Cultural & Historical\", \"Museums, heritage & local experiences\", oc3icon)\noc3icon = Icon(\"landmark\", \"buildings\")\noc4 = OptionCard(\"city\", \"City Break\", \"Urban exploration & nightlife\", oc4icon)\noc4icon = Icon(\"building-2\", \"buildings\")\nfcBudget = FormControl(\"Estimated Budget (per person)\", budgetSlider, \"Drag to set your per-person budget in USD\")\nbudgetSlider = Slider(\"budget\", \"discrete\", 500, 10000, 500, [2500], \"Budget (USD)\")\nfcStay = FormControl(\"Accommodation Preference\", stayChips, \"Select all that apply\")\nstayChips = Chips(\"accommodation\", \"multiple\", [ch1, ch2, ch3, ch4], {}, [\"hotel\"])\nch1 = ChipItem(\"hotel\", \"Hotel\", ch1icon)\nch1icon = Icon(\"building\", \"buildings\")\nch2 = ChipItem(\"resort\", \"Resort\", ch2icon)\nch2icon = Icon(\"umbrella\", \"travel\")\nch3 = ChipItem(\"airbnb\", \"Vacation Rental\", ch3icon)\nch3icon = Icon(\"home\", \"home\")\nch4 = ChipItem(\"camping\", \"Camping / Glamping\", ch4icon)\nch4icon = Icon(\"tent\", \"nature\")\nfcNotes = FormControl(\"Special Requests or Notes\", notesArea, \"Any dietary needs, accessibility requirements, or specific interests?\")\nnotesArea = TextArea(\"special-requests\", \"e.g. vegetarian meals, wheelchair access, honeymoon surprise...\", 4)","checkpoints":[{"at":361,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Plan Your Trip","subtitle":"A few ideas"},"partial":true,"hasDynamicProps":false,"statementId":"header"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["intro","inspoCards","form"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":722,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Plan Your Trip","subtitle":"A few ideas to spark inspiration — then tell me your preferences"},"partial":true,"hasDynamicProps":false,"statementId":"header"},{"type":"element","typeName":"TextContent","props":{"text":"Tap a style to jump straight in, or fill the quick form below for a tailored plan."},"partial":true,"hasDynamicProps":false,"statementId":"intro"},{"type":"element","typeName":"VisualCardBlock","props":{"items":[],"layout":"grid","responsive":true,"action":{"type":"continue_conversation","context":"Plan me a trip in this style with destinations, itinerary, and costs"}},"partial":true,"hasDynamicProps":false,"statementId":"inspoCards"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["i","insp2","form"],"orphaned":[],"statementCount":5,"errors":[{"code":"null-required","component":"VisualCardItem","path":"/body","message":"required field \"/body\" cannot be null — signature: VisualCardItem(body*: BoldText, id: string, bgImageSrc: string, tag: Tag, bgImageAlt: string)","statementId":"insp1"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":1083,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Plan Your Trip","subtitle":"A few ideas to spark inspiration — then tell me your preferences"},"partial":true,"hasDynamicProps":false,"statementId":"header"},{"type":"element","typeName":"TextContent","props":{"text":"Tap a style to jump straight in, or fill the quick form below for a tailored plan."},"partial":true,"hasDynamicProps":false,"statementId":"intro"},{"type":"element","typeName":"VisualCardBlock","props":{"items":[{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Tropical Beach Escape","subtext":"Overwater villas & snorkeling","subtextVariant":"text","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"insp1body"},"id":"inspo-beach","bgImageSrc":"https://www.travelandleisure.com/thmb/cuAB7XMQ6s_Gji-no956KNopT7M=/1500x0/filters:no_upscale():max_bytes(150000):strip_icc()/TAL-maldives-TROPVACAY0325-1b5047d5931d4850b47d082966f9f563.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Re"},"partial":true,"hasDynamicProps":false,"statementId":"insp1tag"},"bgImageAlt":"Maldives overwater villas"},"partial":true,"hasDynamicProps":false,"statementId":"insp1"}],"layout":"grid","responsive":true,"action":{"type":"continue_conversation","context":"Plan me a trip in this style with destinations, itinerary, and costs"}},"partial":true,"hasDynamicProps":false,"statementId":"inspoCards"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["insp2","form"],"orphaned":[],"statementCount":7,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":1444,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Plan Your Trip","subtitle":"A few ideas to spark inspiration — then tell me your preferences"},"partial":true,"hasDynamicProps":false,"statementId":"header"},{"type":"element","typeName":"TextContent","props":{"text":"Tap a style to jump straight in, or fill the quick form below for a tailored plan."},"partial":true,"hasDynamicProps":false,"statementId":"intro"},{"type":"element","typeName":"VisualCardBlock","props":{"items":[{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Tropical Beach Escape","subtext":"Overwater villas & snorkeling","subtextVariant":"text","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"insp1body"},"id":"inspo-beach","bgImageSrc":"https://www.travelandleisure.com/thmb/cuAB7XMQ6s_Gji-no956KNopT7M=/1500x0/filters:no_upscale():max_bytes(150000):strip_icc()/TAL-maldives-TROPVACAY0325-1b5047d5931d4850b47d082966f9f563.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Relaxation","icon":{"type":"element","typeName":"Icon","props":{"name":"umbrella","category":"travel"},"partial":true,"hasDynamicProps":false,"statementId":"insp1icon"},"size":"sm","variant":"info"},"partial":true,"hasDynamicProps":false,"statementId":"insp1tag"},"bgImageAlt":"Maldives overwater villas"},"partial":true,"hasDynamicProps":false,"statementId":"insp1"},{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Mountain Adventure","subtext":"Trekking & epic views","subtextVariant":"text","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"insp2body"},"id":"inspo-mountain","bgImageSrc":"https://www.salkantaytrekmachu.com/img/adventure-travel-peru-032.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Active","icon":null,"size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"insp2tag"},"bgImageAlt":"Trekking in the Peruvian Andes"},"partial":true,"hasDynamicProps":false,"statementId":"insp2"}],"layout":"grid","responsive":true,"action":{"type":"continue_conversation","context":"Plan me a trip in this style with destinations, itinerary, and costs"}},"partial":true,"hasDynamicProps":false,"statementId":"inspoCards"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["insp2icon","form"],"orphaned":[],"statementCount":11,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":1805,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Plan Your Trip","subtitle":"A few ideas to spark inspiration — then tell me your preferences"},"partial":false,"hasDynamicProps":false,"statementId":"header"},{"type":"element","typeName":"TextContent","props":{"text":"Tap a style to jump straight in, or fill the quick form below for a tailored plan."},"partial":false,"hasDynamicProps":false,"statementId":"intro"},{"type":"element","typeName":"VisualCardBlock","props":{"items":[{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Tropical Beach Escape","subtext":"Overwater villas & snorkeling","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"insp1body"},"id":"inspo-beach","bgImageSrc":"https://www.travelandleisure.com/thmb/cuAB7XMQ6s_Gji-no956KNopT7M=/1500x0/filters:no_upscale():max_bytes(150000):strip_icc()/TAL-maldives-TROPVACAY0325-1b5047d5931d4850b47d082966f9f563.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Relaxation","icon":{"type":"element","typeName":"Icon","props":{"name":"umbrella","category":"travel"},"partial":false,"hasDynamicProps":false,"statementId":"insp1icon"},"size":"sm","variant":"info"},"partial":false,"hasDynamicProps":false,"statementId":"insp1tag"},"bgImageAlt":"Maldives overwater villas"},"partial":false,"hasDynamicProps":false,"statementId":"insp1"},{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Mountain Adventure","subtext":"Trekking & epic views","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"insp2body"},"id":"inspo-mountain","bgImageSrc":"https://www.salkantaytrekmachu.com/img/adventure-travel-peru-032.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Active","icon":{"type":"element","typeName":"Icon","props":{"name":"mountain","category":"nature"},"partial":false,"hasDynamicProps":false,"statementId":"insp2icon"},"size":"sm","variant":"warning"},"partial":false,"hasDynamicProps":false,"statementId":"insp2tag"},"bgImageAlt":"Trekking in the Peruvian Andes"},"partial":false,"hasDynamicProps":false,"statementId":"insp2"}],"layout":"grid","responsive":true,"action":{"type":"continue_conversation","context":"Plan me a trip in this style with destinations, itinerary, and costs"}},"partial":false,"hasDynamicProps":false,"statementId":"inspoCards"},{"type":"element","typeName":"Form","props":{"name":"trip-planner","buttons":{"type":"element","typeName":"Buttons","props":{"buttons":[{"type":"element","typeName":"Button","props":{"label":"Plan My Trip","action":{"type":"continue_conversation","context":"Help me plan a trip based on my form submission"},"variant":"primary"},"partial":false,"hasDynamicProps":false,"statementId":"btnSubmit"}],"direction":"row"},"partial":false,"hasDynamicProps":false,"statementId":"formButtons"},"fields":[]},"partial":false,"hasDynamicProps":false,"statementId":"form"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["btnSkip","fcDestination","fcDates","fcTravellers","fcTripType","fcBudget","fcStay","fcNotes"],"orphaned":[],"statementCount":15,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":2166,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Plan Your Trip","subtitle":"A few ideas to spark inspiration — then tell me your preferences"},"partial":false,"hasDynamicProps":false,"statementId":"header"},{"type":"element","typeName":"TextContent","props":{"text":"Tap a style to jump straight in, or fill the quick form below for a tailored plan."},"partial":false,"hasDynamicProps":false,"statementId":"intro"},{"type":"element","typeName":"VisualCardBlock","props":{"items":[{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Tropical Beach Escape","subtext":"Overwater villas & snorkeling","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"insp1body"},"id":"inspo-beach","bgImageSrc":"https://www.travelandleisure.com/thmb/cuAB7XMQ6s_Gji-no956KNopT7M=/1500x0/filters:no_upscale():max_bytes(150000):strip_icc()/TAL-maldives-TROPVACAY0325-1b5047d5931d4850b47d082966f9f563.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Relaxation","icon":{"type":"element","typeName":"Icon","props":{"name":"umbrella","category":"travel"},"partial":false,"hasDynamicProps":false,"statementId":"insp1icon"},"size":"sm","variant":"info"},"partial":false,"hasDynamicProps":false,"statementId":"insp1tag"},"bgImageAlt":"Maldives overwater villas"},"partial":false,"hasDynamicProps":false,"statementId":"insp1"},{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Mountain Adventure","subtext":"Trekking & epic views","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"insp2body"},"id":"inspo-mountain","bgImageSrc":"https://www.salkantaytrekmachu.com/img/adventure-travel-peru-032.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Active","icon":{"type":"element","typeName":"Icon","props":{"name":"mountain","category":"nature"},"partial":false,"hasDynamicProps":false,"statementId":"insp2icon"},"size":"sm","variant":"warning"},"partial":false,"hasDynamicProps":false,"statementId":"insp2tag"},"bgImageAlt":"Trekking in the Peruvian Andes"},"partial":false,"hasDynamicProps":false,"statementId":"insp2"}],"layout":"grid","responsive":true,"action":{"type":"continue_conversation","context":"Plan me a trip in this style with destinations, itinerary, and costs"}},"partial":false,"hasDynamicProps":false,"statementId":"inspoCards"},{"type":"element","typeName":"Form","props":{"name":"trip-planner","buttons":{"type":"element","typeName":"Buttons","props":{"buttons":[{"type":"element","typeName":"Button","props":{"label":"Plan My Trip","action":{"type":"continue_conversation","context":"Help me plan a trip based on my form submission"},"variant":"primary"},"partial":false,"hasDynamicProps":false,"statementId":"btnSubmit"},{"type":"element","typeName":"Button","props":{"label":"Just Surprise Me","action":{"type":"continue_conversation","context":"Skip the form and surprise me with a destination and itinerary"},"variant":"secondary"},"partial":false,"hasDynamicProps":false,"statementId":"btnSkip"}],"direction":"row"},"partial":false,"hasDynamicProps":false,"statementId":"formButtons"},"fields":[{"type":"element","typeName":"FormControl","props":{"label":"Destination","input":{"type":"element","typeName":"Input","props":{"name":"destination","placeholder":"e.g. Tokyo, Japan — or leave blank","type":"text"},"partial":false,"hasDynamicProps":false,"statementId":"destinationInput"},"hint":"Leave blank if you'd like suggestions"},"partial":false,"hasDynamicProps":false,"statementId":"fcDestination"}]},"partial":false,"hasDynamicProps":false,"statementId":"form"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["Fo","fcTravellers","fcTripType","fcBudget","fcStay","fcNotes"],"orphaned":[],"statementCount":19,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":2527,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Plan Your Trip","subtitle":"A few ideas to spark inspiration — then tell me your preferences"},"partial":true,"hasDynamicProps":false,"statementId":"header"},{"type":"element","typeName":"TextContent","props":{"text":"Tap a style to jump straight in, or fill the quick form below for a tailored plan."},"partial":true,"hasDynamicProps":false,"statementId":"intro"},{"type":"element","typeName":"VisualCardBlock","props":{"items":[{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Tropical Beach Escape","subtext":"Overwater villas & snorkeling","subtextVariant":"text","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"insp1body"},"id":"inspo-beach","bgImageSrc":"https://www.travelandleisure.com/thmb/cuAB7XMQ6s_Gji-no956KNopT7M=/1500x0/filters:no_upscale():max_bytes(150000):strip_icc()/TAL-maldives-TROPVACAY0325-1b5047d5931d4850b47d082966f9f563.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Relaxation","icon":{"type":"element","typeName":"Icon","props":{"name":"umbrella","category":"travel"},"partial":true,"hasDynamicProps":false,"statementId":"insp1icon"},"size":"sm","variant":"info"},"partial":true,"hasDynamicProps":false,"statementId":"insp1tag"},"bgImageAlt":"Maldives overwater villas"},"partial":true,"hasDynamicProps":false,"statementId":"insp1"},{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Mountain Adventure","subtext":"Trekking & epic views","subtextVariant":"text","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"insp2body"},"id":"inspo-mountain","bgImageSrc":"https://www.salkantaytrekmachu.com/img/adventure-travel-peru-032.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Active","icon":{"type":"element","typeName":"Icon","props":{"name":"mountain","category":"nature"},"partial":true,"hasDynamicProps":false,"statementId":"insp2icon"},"size":"sm","variant":"warning"},"partial":true,"hasDynamicProps":false,"statementId":"insp2tag"},"bgImageAlt":"Trekking in the Peruvian Andes"},"partial":true,"hasDynamicProps":false,"statementId":"insp2"}],"layout":"grid","responsive":true,"action":{"type":"continue_conversation","context":"Plan me a trip in this style with destinations, itinerary, and costs"}},"partial":true,"hasDynamicProps":false,"statementId":"inspoCards"},{"type":"element","typeName":"Form","props":{"name":"trip-planner","buttons":{"type":"element","typeName":"Buttons","props":{"buttons":[{"type":"element","typeName":"Button","props":{"label":"Plan My Trip","action":{"type":"continue_conversation","context":"Help me plan a trip based on my form submission"},"variant":"primary"},"partial":true,"hasDynamicProps":false,"statementId":"btnSubmit"},{"type":"element","typeName":"Button","props":{"label":"Just Surprise Me","action":{"type":"continue_conversation","context":"Skip the form and surprise me with a destination and itinerary"},"variant":"secondary"},"partial":true,"hasDynamicProps":false,"statementId":"btnSkip"}],"direction":"row"},"partial":true,"hasDynamicProps":false,"statementId":"formButtons"},"fields":[{"type":"element","typeName":"FormControl","props":{"label":"Destination","input":{"type":"element","typeName":"Input","props":{"name":"destination","placeholder":"e.g. Tokyo, Japan — or leave blank","type":"text"},"partial":true,"hasDynamicProps":false,"statementId":"destinationInput"},"hint":"Leave blank if you'd like suggestions"},"partial":true,"hasDynamicProps":false,"statementId":"fcDestination"},{"type":"element","typeName":"FormControl","props":{"label":"Travel Dates","input":{"type":"element","typeName":"DatePicker","props":{"name":"travel-dates","mode":"range","rules":{"required":true}},"partial":true,"hasDynamicProps":false,"statementId":"datesPicker"},"hint":"Select your start and end dates"},"partial":true,"hasDynamicProps":false,"statementId":"fcDates"},{"type":"element","typeName":"FormControl","props":{"label":"Number of Travellers","input":{"type":"element","typeName":"Select","props":{"name":"num-travellers","items":[],"placeholder":"Select number of travellers","rules":{"required":true}},"partial":true,"hasDynamicProps":false,"statementId":"travellersSelect"},"hint":"How many people are travelling?"},"partial":true,"hasDynamicProps":false,"statementId":"fcTravellers"}]},"partial":true,"hasDynamicProps":false,"statementId":"form"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["tr1","tr2","tr3","tr4","fcTripType","fcBudget","fcStay","fcNotes"],"orphaned":[],"statementCount":22,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":2888,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Plan Your Trip","subtitle":"A few ideas to spark inspiration — then tell me your preferences"},"partial":true,"hasDynamicProps":false,"statementId":"header"},{"type":"element","typeName":"TextContent","props":{"text":"Tap a style to jump straight in, or fill the quick form below for a tailored plan."},"partial":true,"hasDynamicProps":false,"statementId":"intro"},{"type":"element","typeName":"VisualCardBlock","props":{"items":[{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Tropical Beach Escape","subtext":"Overwater villas & snorkeling","subtextVariant":"text","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"insp1body"},"id":"inspo-beach","bgImageSrc":"https://www.travelandleisure.com/thmb/cuAB7XMQ6s_Gji-no956KNopT7M=/1500x0/filters:no_upscale():max_bytes(150000):strip_icc()/TAL-maldives-TROPVACAY0325-1b5047d5931d4850b47d082966f9f563.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Relaxation","icon":{"type":"element","typeName":"Icon","props":{"name":"umbrella","category":"travel"},"partial":true,"hasDynamicProps":false,"statementId":"insp1icon"},"size":"sm","variant":"info"},"partial":true,"hasDynamicProps":false,"statementId":"insp1tag"},"bgImageAlt":"Maldives overwater villas"},"partial":true,"hasDynamicProps":false,"statementId":"insp1"},{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Mountain Adventure","subtext":"Trekking & epic views","subtextVariant":"text","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"insp2body"},"id":"inspo-mountain","bgImageSrc":"https://www.salkantaytrekmachu.com/img/adventure-travel-peru-032.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Active","icon":{"type":"element","typeName":"Icon","props":{"name":"mountain","category":"nature"},"partial":true,"hasDynamicProps":false,"statementId":"insp2icon"},"size":"sm","variant":"warning"},"partial":true,"hasDynamicProps":false,"statementId":"insp2tag"},"bgImageAlt":"Trekking in the Peruvian Andes"},"partial":true,"hasDynamicProps":false,"statementId":"insp2"}],"layout":"grid","responsive":true,"action":{"type":"continue_conversation","context":"Plan me a trip in this style with destinations, itinerary, and costs"}},"partial":true,"hasDynamicProps":false,"statementId":"inspoCards"},{"type":"element","typeName":"Form","props":{"name":"trip-planner","buttons":{"type":"element","typeName":"Buttons","props":{"buttons":[{"type":"element","typeName":"Button","props":{"label":"Plan My Trip","action":{"type":"continue_conversation","context":"Help me plan a trip based on my form submission"},"variant":"primary"},"partial":true,"hasDynamicProps":false,"statementId":"btnSubmit"},{"type":"element","typeName":"Button","props":{"label":"Just Surprise Me","action":{"type":"continue_conversation","context":"Skip the form and surprise me with a destination and itinerary"},"variant":"secondary"},"partial":true,"hasDynamicProps":false,"statementId":"btnSkip"}],"direction":"row"},"partial":true,"hasDynamicProps":false,"statementId":"formButtons"},"fields":[{"type":"element","typeName":"FormControl","props":{"label":"Destination","input":{"type":"element","typeName":"Input","props":{"name":"destination","placeholder":"e.g. Tokyo, Japan — or leave blank","type":"text"},"partial":true,"hasDynamicProps":false,"statementId":"destinationInput"},"hint":"Leave blank if you'd like suggestions"},"partial":true,"hasDynamicProps":false,"statementId":"fcDestination"},{"type":"element","typeName":"FormControl","props":{"label":"Travel Dates","input":{"type":"element","typeName":"DatePicker","props":{"name":"travel-dates","mode":"range","rules":{"required":true}},"partial":true,"hasDynamicProps":false,"statementId":"datesPicker"},"hint":"Select your start and end dates"},"partial":true,"hasDynamicProps":false,"statementId":"fcDates"},{"type":"element","typeName":"FormControl","props":{"label":"Number of Travellers","input":{"type":"element","typeName":"Select","props":{"name":"num-travellers","items":[{"type":"element","typeName":"SelectItem","props":{"value":"1","label":"1 — Solo"},"partial":true,"hasDynamicProps":false,"statementId":"tr1"},{"type":"element","typeName":"SelectItem","props":{"value":"2","label":"2 — Couple"},"partial":true,"hasDynamicProps":false,"statementId":"tr2"},{"type":"element","typeName":"SelectItem","props":{"value":"3-4","label":"3–4 People"},"partial":true,"hasDynamicProps":false,"statementId":"tr3"},{"type":"element","typeName":"SelectItem","props":{"value":"5+","label":"5+ People"},"partial":true,"hasDynamicProps":false,"statementId":"tr4"}],"placeholder":"Select number of travellers","rules":{"required":true}},"partial":true,"hasDynamicProps":false,"statementId":"travellersSelect"},"hint":"How many people are travelling?"},"partial":true,"hasDynamicProps":false,"statementId":"fcTravellers"},{"type":"element","typeName":"FormControl","props":{"label":"Trip Type","input":{"type":"element","typeName":"OptionCards","props":{"name":"trip-type","type":"single","items":[],"rules":{"required":true}},"partial":true,"hasDynamicProps":false,"statementId":"tripTypeCards"},"hint":"What kind of trip are you planning?"},"partial":true,"hasDynamicProps":false,"statementId":"fcTripType"}]},"partial":true,"hasDynamicProps":false,"statementId":"form"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["oc2","oc3","oc4","fcBudget","fcStay","fcNotes"],"orphaned":[],"statementCount":29,"errors":[{"code":"missing-required","component":"OptionCard","path":"/title","message":"missing required field \"/title\" — signature: OptionCard(value*: string, title*: string, subtitle: string, topContent, disabled: boolean)","statementId":"oc1"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":3249,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Plan Your Trip","subtitle":"A few ideas to spark inspiration — then tell me your preferences"},"partial":false,"hasDynamicProps":false,"statementId":"header"},{"type":"element","typeName":"TextContent","props":{"text":"Tap a style to jump straight in, or fill the quick form below for a tailored plan."},"partial":false,"hasDynamicProps":false,"statementId":"intro"},{"type":"element","typeName":"VisualCardBlock","props":{"items":[{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Tropical Beach Escape","subtext":"Overwater villas & snorkeling","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"insp1body"},"id":"inspo-beach","bgImageSrc":"https://www.travelandleisure.com/thmb/cuAB7XMQ6s_Gji-no956KNopT7M=/1500x0/filters:no_upscale():max_bytes(150000):strip_icc()/TAL-maldives-TROPVACAY0325-1b5047d5931d4850b47d082966f9f563.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Relaxation","icon":{"type":"element","typeName":"Icon","props":{"name":"umbrella","category":"travel"},"partial":false,"hasDynamicProps":false,"statementId":"insp1icon"},"size":"sm","variant":"info"},"partial":false,"hasDynamicProps":false,"statementId":"insp1tag"},"bgImageAlt":"Maldives overwater villas"},"partial":false,"hasDynamicProps":false,"statementId":"insp1"},{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Mountain Adventure","subtext":"Trekking & epic views","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"insp2body"},"id":"inspo-mountain","bgImageSrc":"https://www.salkantaytrekmachu.com/img/adventure-travel-peru-032.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Active","icon":{"type":"element","typeName":"Icon","props":{"name":"mountain","category":"nature"},"partial":false,"hasDynamicProps":false,"statementId":"insp2icon"},"size":"sm","variant":"warning"},"partial":false,"hasDynamicProps":false,"statementId":"insp2tag"},"bgImageAlt":"Trekking in the Peruvian Andes"},"partial":false,"hasDynamicProps":false,"statementId":"insp2"}],"layout":"grid","responsive":true,"action":{"type":"continue_conversation","context":"Plan me a trip in this style with destinations, itinerary, and costs"}},"partial":false,"hasDynamicProps":false,"statementId":"inspoCards"},{"type":"element","typeName":"Form","props":{"name":"trip-planner","buttons":{"type":"element","typeName":"Buttons","props":{"buttons":[{"type":"element","typeName":"Button","props":{"label":"Plan My Trip","action":{"type":"continue_conversation","context":"Help me plan a trip based on my form submission"},"variant":"primary"},"partial":false,"hasDynamicProps":false,"statementId":"btnSubmit"},{"type":"element","typeName":"Button","props":{"label":"Just Surprise Me","action":{"type":"continue_conversation","context":"Skip the form and surprise me with a destination and itinerary"},"variant":"secondary"},"partial":false,"hasDynamicProps":false,"statementId":"btnSkip"}],"direction":"row"},"partial":false,"hasDynamicProps":false,"statementId":"formButtons"},"fields":[{"type":"element","typeName":"FormControl","props":{"label":"Destination","input":{"type":"element","typeName":"Input","props":{"name":"destination","placeholder":"e.g. Tokyo, Japan — or leave blank","type":"text"},"partial":false,"hasDynamicProps":false,"statementId":"destinationInput"},"hint":"Leave blank if you'd like suggestions"},"partial":false,"hasDynamicProps":false,"statementId":"fcDestination"},{"type":"element","typeName":"FormControl","props":{"label":"Travel Dates","input":{"type":"element","typeName":"DatePicker","props":{"name":"travel-dates","mode":"range","rules":{"required":true}},"partial":false,"hasDynamicProps":false,"statementId":"datesPicker"},"hint":"Select your start and end dates"},"partial":false,"hasDynamicProps":false,"statementId":"fcDates"},{"type":"element","typeName":"FormControl","props":{"label":"Number of Travellers","input":{"type":"element","typeName":"Select","props":{"name":"num-travellers","items":[{"type":"element","typeName":"SelectItem","props":{"value":"1","label":"1 — Solo"},"partial":false,"hasDynamicProps":false,"statementId":"tr1"},{"type":"element","typeName":"SelectItem","props":{"value":"2","label":"2 — Couple"},"partial":false,"hasDynamicProps":false,"statementId":"tr2"},{"type":"element","typeName":"SelectItem","props":{"value":"3-4","label":"3–4 People"},"partial":false,"hasDynamicProps":false,"statementId":"tr3"},{"type":"element","typeName":"SelectItem","props":{"value":"5+","label":"5+ People"},"partial":false,"hasDynamicProps":false,"statementId":"tr4"}],"placeholder":"Select number of travellers","rules":{"required":true}},"partial":false,"hasDynamicProps":false,"statementId":"travellersSelect"},"hint":"How many people are travelling?"},"partial":false,"hasDynamicProps":false,"statementId":"fcTravellers"},{"type":"element","typeName":"FormControl","props":{"label":"Trip Type","input":{"type":"element","typeName":"OptionCards","props":{"name":"trip-type","type":"single","items":[{"type":"element","typeName":"OptionCard","props":{"value":"adventure","title":"Adventure","subtitle":"Hiking, trekking & outdoor activities","topContent":{"type":"element","typeName":"Icon","props":{"name":"mountain","category":"nature"},"partial":false,"hasDynamicProps":false,"statementId":"oc1icon"}},"partial":false,"hasDynamicProps":false,"statementId":"oc1"},{"type":"element","typeName":"OptionCard","props":{"value":"beach","title":"Beach & Relaxation","subtitle":"Sun, sand & leisure","topContent":{"type":"element","typeName":"Icon","props":{"name":"waves","category":"nature"},"partial":false,"hasDynamicProps":false,"statementId":"oc2icon"}},"partial":false,"hasDynamicProps":false,"statementId":"oc2"},{"type":"element","typeName":"OptionCard","props":{"value":"cultural","title":"Cultural & Historical","subtitle":"Museums, heritage & local experiences","topContent":{"type":"element","typeName":"Icon","props":{"name":"landmark","category":"buildings"},"partial":false,"hasDynamicProps":false,"statementId":"oc3icon"}},"partial":false,"hasDynamicProps":false,"statementId":"oc3"}],"rules":{"required":true}},"partial":false,"hasDynamicProps":false,"statementId":"tripTypeCards"},"hint":"What kind of trip are you planning?"},"partial":false,"hasDynamicProps":false,"statementId":"fcTripType"}]},"partial":false,"hasDynamicProps":false,"statementId":"form"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["oc4","fcBudget","fcStay","fcNotes"],"orphaned":[],"statementCount":34,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":3610,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Plan Your Trip","subtitle":"A few ideas to spark inspiration — then tell me your preferences"},"partial":true,"hasDynamicProps":false,"statementId":"header"},{"type":"element","typeName":"TextContent","props":{"text":"Tap a style to jump straight in, or fill the quick form below for a tailored plan."},"partial":true,"hasDynamicProps":false,"statementId":"intro"},{"type":"element","typeName":"VisualCardBlock","props":{"items":[{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Tropical Beach Escape","subtext":"Overwater villas & snorkeling","subtextVariant":"text","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"insp1body"},"id":"inspo-beach","bgImageSrc":"https://www.travelandleisure.com/thmb/cuAB7XMQ6s_Gji-no956KNopT7M=/1500x0/filters:no_upscale():max_bytes(150000):strip_icc()/TAL-maldives-TROPVACAY0325-1b5047d5931d4850b47d082966f9f563.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Relaxation","icon":{"type":"element","typeName":"Icon","props":{"name":"umbrella","category":"travel"},"partial":true,"hasDynamicProps":false,"statementId":"insp1icon"},"size":"sm","variant":"info"},"partial":true,"hasDynamicProps":false,"statementId":"insp1tag"},"bgImageAlt":"Maldives overwater villas"},"partial":true,"hasDynamicProps":false,"statementId":"insp1"},{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Mountain Adventure","subtext":"Trekking & epic views","subtextVariant":"text","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"insp2body"},"id":"inspo-mountain","bgImageSrc":"https://www.salkantaytrekmachu.com/img/adventure-travel-peru-032.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Active","icon":{"type":"element","typeName":"Icon","props":{"name":"mountain","category":"nature"},"partial":true,"hasDynamicProps":false,"statementId":"insp2icon"},"size":"sm","variant":"warning"},"partial":true,"hasDynamicProps":false,"statementId":"insp2tag"},"bgImageAlt":"Trekking in the Peruvian Andes"},"partial":true,"hasDynamicProps":false,"statementId":"insp2"}],"layout":"grid","responsive":true,"action":{"type":"continue_conversation","context":"Plan me a trip in this style with destinations, itinerary, and costs"}},"partial":true,"hasDynamicProps":false,"statementId":"inspoCards"},{"type":"element","typeName":"Form","props":{"name":"trip-planner","buttons":{"type":"element","typeName":"Buttons","props":{"buttons":[{"type":"element","typeName":"Button","props":{"label":"Plan My Trip","action":{"type":"continue_conversation","context":"Help me plan a trip based on my form submission"},"variant":"primary"},"partial":true,"hasDynamicProps":false,"statementId":"btnSubmit"},{"type":"element","typeName":"Button","props":{"label":"Just Surprise Me","action":{"type":"continue_conversation","context":"Skip the form and surprise me with a destination and itinerary"},"variant":"secondary"},"partial":true,"hasDynamicProps":false,"statementId":"btnSkip"}],"direction":"row"},"partial":true,"hasDynamicProps":false,"statementId":"formButtons"},"fields":[{"type":"element","typeName":"FormControl","props":{"label":"Destination","input":{"type":"element","typeName":"Input","props":{"name":"destination","placeholder":"e.g. Tokyo, Japan — or leave blank","type":"text"},"partial":true,"hasDynamicProps":false,"statementId":"destinationInput"},"hint":"Leave blank if you'd like suggestions"},"partial":true,"hasDynamicProps":false,"statementId":"fcDestination"},{"type":"element","typeName":"FormControl","props":{"label":"Travel Dates","input":{"type":"element","typeName":"DatePicker","props":{"name":"travel-dates","mode":"range","rules":{"required":true}},"partial":true,"hasDynamicProps":false,"statementId":"datesPicker"},"hint":"Select your start and end dates"},"partial":true,"hasDynamicProps":false,"statementId":"fcDates"},{"type":"element","typeName":"FormControl","props":{"label":"Number of Travellers","input":{"type":"element","typeName":"Select","props":{"name":"num-travellers","items":[{"type":"element","typeName":"SelectItem","props":{"value":"1","label":"1 — Solo"},"partial":true,"hasDynamicProps":false,"statementId":"tr1"},{"type":"element","typeName":"SelectItem","props":{"value":"2","label":"2 — Couple"},"partial":true,"hasDynamicProps":false,"statementId":"tr2"},{"type":"element","typeName":"SelectItem","props":{"value":"3-4","label":"3–4 People"},"partial":true,"hasDynamicProps":false,"statementId":"tr3"},{"type":"element","typeName":"SelectItem","props":{"value":"5+","label":"5+ People"},"partial":true,"hasDynamicProps":false,"statementId":"tr4"}],"placeholder":"Select number of travellers","rules":{"required":true}},"partial":true,"hasDynamicProps":false,"statementId":"travellersSelect"},"hint":"How many people are travelling?"},"partial":true,"hasDynamicProps":false,"statementId":"fcTravellers"},{"type":"element","typeName":"FormControl","props":{"label":"Trip Type","input":{"type":"element","typeName":"OptionCards","props":{"name":"trip-type","type":"single","items":[{"type":"element","typeName":"OptionCard","props":{"value":"adventure","title":"Adventure","subtitle":"Hiking, trekking & outdoor activities","topContent":{"type":"element","typeName":"Icon","props":{"name":"mountain","category":"nature"},"partial":true,"hasDynamicProps":false,"statementId":"oc1icon"}},"partial":true,"hasDynamicProps":false,"statementId":"oc1"},{"type":"element","typeName":"OptionCard","props":{"value":"beach","title":"Beach & Relaxation","subtitle":"Sun, sand & leisure","topContent":{"type":"element","typeName":"Icon","props":{"name":"waves","category":"nature"},"partial":true,"hasDynamicProps":false,"statementId":"oc2icon"}},"partial":true,"hasDynamicProps":false,"statementId":"oc2"},{"type":"element","typeName":"OptionCard","props":{"value":"cultural","title":"Cultural & Historical","subtitle":"Museums, heritage & local experiences","topContent":{"type":"element","typeName":"Icon","props":{"name":"landmark","category":"buildings"},"partial":true,"hasDynamicProps":false,"statementId":"oc3icon"}},"partial":true,"hasDynamicProps":false,"statementId":"oc3"},{"type":"element","typeName":"OptionCard","props":{"value":"city","title":"City Break","subtitle":"Urban exploration & nightlife","topContent":{"type":"element","typeName":"Icon","props":{"name":"building-2","category":"buildings"},"partial":true,"hasDynamicProps":false,"statementId":"oc4icon"}},"partial":true,"hasDynamicProps":false,"statementId":"oc4"}],"rules":{"required":true}},"partial":true,"hasDynamicProps":false,"statementId":"tripTypeCards"},"hint":"What kind of trip are you planning?"},"partial":true,"hasDynamicProps":false,"statementId":"fcTripType"},{"type":"element","typeName":"FormControl","props":{"label":"Estimated Budget (per person)","input":{"type":"element","typeName":"Slider","props":{"name":"budget","variant":"discrete","min":500,"max":10000,"step":500,"defaultValue":[2500],"label":"Budget (USD)"},"partial":true,"hasDynamicProps":false,"statementId":"budgetSlider"},"hint":"Drag to set your per-person budget in USD"},"partial":true,"hasDynamicProps":false,"statementId":"fcBudget"}]},"partial":true,"hasDynamicProps":false,"statementId":"form"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["fcNotes"],"orphaned":[],"statementCount":39,"errors":[{"code":"missing-required","component":"FormControl","path":"/input","message":"missing required field \"/input\" — signature: FormControl(label*: string, input*, hint: string)","statementId":"fcStay"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":3971,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Plan Your Trip","subtitle":"A few ideas to spark inspiration — then tell me your preferences"},"partial":true,"hasDynamicProps":false,"statementId":"header"},{"type":"element","typeName":"TextContent","props":{"text":"Tap a style to jump straight in, or fill the quick form below for a tailored plan."},"partial":true,"hasDynamicProps":false,"statementId":"intro"},{"type":"element","typeName":"VisualCardBlock","props":{"items":[{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Tropical Beach Escape","subtext":"Overwater villas & snorkeling","subtextVariant":"text","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"insp1body"},"id":"inspo-beach","bgImageSrc":"https://www.travelandleisure.com/thmb/cuAB7XMQ6s_Gji-no956KNopT7M=/1500x0/filters:no_upscale():max_bytes(150000):strip_icc()/TAL-maldives-TROPVACAY0325-1b5047d5931d4850b47d082966f9f563.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Relaxation","icon":{"type":"element","typeName":"Icon","props":{"name":"umbrella","category":"travel"},"partial":true,"hasDynamicProps":false,"statementId":"insp1icon"},"size":"sm","variant":"info"},"partial":true,"hasDynamicProps":false,"statementId":"insp1tag"},"bgImageAlt":"Maldives overwater villas"},"partial":true,"hasDynamicProps":false,"statementId":"insp1"},{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Mountain Adventure","subtext":"Trekking & epic views","subtextVariant":"text","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"insp2body"},"id":"inspo-mountain","bgImageSrc":"https://www.salkantaytrekmachu.com/img/adventure-travel-peru-032.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Active","icon":{"type":"element","typeName":"Icon","props":{"name":"mountain","category":"nature"},"partial":true,"hasDynamicProps":false,"statementId":"insp2icon"},"size":"sm","variant":"warning"},"partial":true,"hasDynamicProps":false,"statementId":"insp2tag"},"bgImageAlt":"Trekking in the Peruvian Andes"},"partial":true,"hasDynamicProps":false,"statementId":"insp2"}],"layout":"grid","responsive":true,"action":{"type":"continue_conversation","context":"Plan me a trip in this style with destinations, itinerary, and costs"}},"partial":true,"hasDynamicProps":false,"statementId":"inspoCards"},{"type":"element","typeName":"Form","props":{"name":"trip-planner","buttons":{"type":"element","typeName":"Buttons","props":{"buttons":[{"type":"element","typeName":"Button","props":{"label":"Plan My Trip","action":{"type":"continue_conversation","context":"Help me plan a trip based on my form submission"},"variant":"primary"},"partial":true,"hasDynamicProps":false,"statementId":"btnSubmit"},{"type":"element","typeName":"Button","props":{"label":"Just Surprise Me","action":{"type":"continue_conversation","context":"Skip the form and surprise me with a destination and itinerary"},"variant":"secondary"},"partial":true,"hasDynamicProps":false,"statementId":"btnSkip"}],"direction":"row"},"partial":true,"hasDynamicProps":false,"statementId":"formButtons"},"fields":[{"type":"element","typeName":"FormControl","props":{"label":"Destination","input":{"type":"element","typeName":"Input","props":{"name":"destination","placeholder":"e.g. Tokyo, Japan — or leave blank","type":"text"},"partial":true,"hasDynamicProps":false,"statementId":"destinationInput"},"hint":"Leave blank if you'd like suggestions"},"partial":true,"hasDynamicProps":false,"statementId":"fcDestination"},{"type":"element","typeName":"FormControl","props":{"label":"Travel Dates","input":{"type":"element","typeName":"DatePicker","props":{"name":"travel-dates","mode":"range","rules":{"required":true}},"partial":true,"hasDynamicProps":false,"statementId":"datesPicker"},"hint":"Select your start and end dates"},"partial":true,"hasDynamicProps":false,"statementId":"fcDates"},{"type":"element","typeName":"FormControl","props":{"label":"Number of Travellers","input":{"type":"element","typeName":"Select","props":{"name":"num-travellers","items":[{"type":"element","typeName":"SelectItem","props":{"value":"1","label":"1 — Solo"},"partial":true,"hasDynamicProps":false,"statementId":"tr1"},{"type":"element","typeName":"SelectItem","props":{"value":"2","label":"2 — Couple"},"partial":true,"hasDynamicProps":false,"statementId":"tr2"},{"type":"element","typeName":"SelectItem","props":{"value":"3-4","label":"3–4 People"},"partial":true,"hasDynamicProps":false,"statementId":"tr3"},{"type":"element","typeName":"SelectItem","props":{"value":"5+","label":"5+ People"},"partial":true,"hasDynamicProps":false,"statementId":"tr4"}],"placeholder":"Select number of travellers","rules":{"required":true}},"partial":true,"hasDynamicProps":false,"statementId":"travellersSelect"},"hint":"How many people are travelling?"},"partial":true,"hasDynamicProps":false,"statementId":"fcTravellers"},{"type":"element","typeName":"FormControl","props":{"label":"Trip Type","input":{"type":"element","typeName":"OptionCards","props":{"name":"trip-type","type":"single","items":[{"type":"element","typeName":"OptionCard","props":{"value":"adventure","title":"Adventure","subtitle":"Hiking, trekking & outdoor activities","topContent":{"type":"element","typeName":"Icon","props":{"name":"mountain","category":"nature"},"partial":true,"hasDynamicProps":false,"statementId":"oc1icon"}},"partial":true,"hasDynamicProps":false,"statementId":"oc1"},{"type":"element","typeName":"OptionCard","props":{"value":"beach","title":"Beach & Relaxation","subtitle":"Sun, sand & leisure","topContent":{"type":"element","typeName":"Icon","props":{"name":"waves","category":"nature"},"partial":true,"hasDynamicProps":false,"statementId":"oc2icon"}},"partial":true,"hasDynamicProps":false,"statementId":"oc2"},{"type":"element","typeName":"OptionCard","props":{"value":"cultural","title":"Cultural & Historical","subtitle":"Museums, heritage & local experiences","topContent":{"type":"element","typeName":"Icon","props":{"name":"landmark","category":"buildings"},"partial":true,"hasDynamicProps":false,"statementId":"oc3icon"}},"partial":true,"hasDynamicProps":false,"statementId":"oc3"},{"type":"element","typeName":"OptionCard","props":{"value":"city","title":"City Break","subtitle":"Urban exploration & nightlife","topContent":{"type":"element","typeName":"Icon","props":{"name":"building-2","category":"buildings"},"partial":true,"hasDynamicProps":false,"statementId":"oc4icon"}},"partial":true,"hasDynamicProps":false,"statementId":"oc4"}],"rules":{"required":true}},"partial":true,"hasDynamicProps":false,"statementId":"tripTypeCards"},"hint":"What kind of trip are you planning?"},"partial":true,"hasDynamicProps":false,"statementId":"fcTripType"},{"type":"element","typeName":"FormControl","props":{"label":"Estimated Budget (per person)","input":{"type":"element","typeName":"Slider","props":{"name":"budget","variant":"discrete","min":500,"max":10000,"step":500,"defaultValue":[2500],"label":"Budget (USD)"},"partial":true,"hasDynamicProps":false,"statementId":"budgetSlider"},"hint":"Drag to set your per-person budget in USD"},"partial":true,"hasDynamicProps":false,"statementId":"fcBudget"},{"type":"element","typeName":"FormControl","props":{"label":"Accommodation Preference","input":{"type":"element","typeName":"Chips","props":{"name":"accommodation","type":"multiple","items":[{"type":"element","typeName":"ChipItem","props":{"value":"hotel","label":"Hotel","icon":{"type":"element","typeName":"Icon","props":{"name":"building","category":"buildings"},"partial":true,"hasDynamicProps":false,"statementId":"ch1icon"}},"partial":true,"hasDynamicProps":false,"statementId":"ch1"},{"type":"element","typeName":"ChipItem","props":{"value":"resort","label":"Resort","icon":{"type":"element","typeName":"Icon","props":{"name":"umbrella","category":"travel"},"partial":true,"hasDynamicProps":false,"statementId":"ch2icon"}},"partial":true,"hasDynamicProps":false,"statementId":"ch2"},{"type":"element","typeName":"ChipItem","props":{"value":"airbnb","label":"Vacation Rental","icon":null},"partial":true,"hasDynamicProps":false,"statementId":"ch3"}],"rules":{},"defaultValue":["hotel"]},"partial":true,"hasDynamicProps":false,"statementId":"stayChips"},"hint":"Select all that apply"},"partial":true,"hasDynamicProps":false,"statementId":"fcStay"}]},"partial":true,"hasDynamicProps":false,"statementId":"form"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["ch4","fcNotes"],"orphaned":[],"statementCount":46,"errors":[{"code":"missing-required","component":"Icon","path":"/name","message":"missing required field \"/name\" — signature: Icon(name*: string, category: string)","statementId":"ch3icon"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":4322,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Plan Your Trip","subtitle":"A few ideas to spark inspiration — then tell me your preferences"},"partial":false,"hasDynamicProps":false,"statementId":"header"},{"type":"element","typeName":"TextContent","props":{"text":"Tap a style to jump straight in, or fill the quick form below for a tailored plan."},"partial":false,"hasDynamicProps":false,"statementId":"intro"},{"type":"element","typeName":"VisualCardBlock","props":{"items":[{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Tropical Beach Escape","subtext":"Overwater villas & snorkeling","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"insp1body"},"id":"inspo-beach","bgImageSrc":"https://www.travelandleisure.com/thmb/cuAB7XMQ6s_Gji-no956KNopT7M=/1500x0/filters:no_upscale():max_bytes(150000):strip_icc()/TAL-maldives-TROPVACAY0325-1b5047d5931d4850b47d082966f9f563.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Relaxation","icon":{"type":"element","typeName":"Icon","props":{"name":"umbrella","category":"travel"},"partial":false,"hasDynamicProps":false,"statementId":"insp1icon"},"size":"sm","variant":"info"},"partial":false,"hasDynamicProps":false,"statementId":"insp1tag"},"bgImageAlt":"Maldives overwater villas"},"partial":false,"hasDynamicProps":false,"statementId":"insp1"},{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Mountain Adventure","subtext":"Trekking & epic views","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"insp2body"},"id":"inspo-mountain","bgImageSrc":"https://www.salkantaytrekmachu.com/img/adventure-travel-peru-032.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Active","icon":{"type":"element","typeName":"Icon","props":{"name":"mountain","category":"nature"},"partial":false,"hasDynamicProps":false,"statementId":"insp2icon"},"size":"sm","variant":"warning"},"partial":false,"hasDynamicProps":false,"statementId":"insp2tag"},"bgImageAlt":"Trekking in the Peruvian Andes"},"partial":false,"hasDynamicProps":false,"statementId":"insp2"}],"layout":"grid","responsive":true,"action":{"type":"continue_conversation","context":"Plan me a trip in this style with destinations, itinerary, and costs"}},"partial":false,"hasDynamicProps":false,"statementId":"inspoCards"},{"type":"element","typeName":"Form","props":{"name":"trip-planner","buttons":{"type":"element","typeName":"Buttons","props":{"buttons":[{"type":"element","typeName":"Button","props":{"label":"Plan My Trip","action":{"type":"continue_conversation","context":"Help me plan a trip based on my form submission"},"variant":"primary"},"partial":false,"hasDynamicProps":false,"statementId":"btnSubmit"},{"type":"element","typeName":"Button","props":{"label":"Just Surprise Me","action":{"type":"continue_conversation","context":"Skip the form and surprise me with a destination and itinerary"},"variant":"secondary"},"partial":false,"hasDynamicProps":false,"statementId":"btnSkip"}],"direction":"row"},"partial":false,"hasDynamicProps":false,"statementId":"formButtons"},"fields":[{"type":"element","typeName":"FormControl","props":{"label":"Destination","input":{"type":"element","typeName":"Input","props":{"name":"destination","placeholder":"e.g. Tokyo, Japan — or leave blank","type":"text"},"partial":false,"hasDynamicProps":false,"statementId":"destinationInput"},"hint":"Leave blank if you'd like suggestions"},"partial":false,"hasDynamicProps":false,"statementId":"fcDestination"},{"type":"element","typeName":"FormControl","props":{"label":"Travel Dates","input":{"type":"element","typeName":"DatePicker","props":{"name":"travel-dates","mode":"range","rules":{"required":true}},"partial":false,"hasDynamicProps":false,"statementId":"datesPicker"},"hint":"Select your start and end dates"},"partial":false,"hasDynamicProps":false,"statementId":"fcDates"},{"type":"element","typeName":"FormControl","props":{"label":"Number of Travellers","input":{"type":"element","typeName":"Select","props":{"name":"num-travellers","items":[{"type":"element","typeName":"SelectItem","props":{"value":"1","label":"1 — Solo"},"partial":false,"hasDynamicProps":false,"statementId":"tr1"},{"type":"element","typeName":"SelectItem","props":{"value":"2","label":"2 — Couple"},"partial":false,"hasDynamicProps":false,"statementId":"tr2"},{"type":"element","typeName":"SelectItem","props":{"value":"3-4","label":"3–4 People"},"partial":false,"hasDynamicProps":false,"statementId":"tr3"},{"type":"element","typeName":"SelectItem","props":{"value":"5+","label":"5+ People"},"partial":false,"hasDynamicProps":false,"statementId":"tr4"}],"placeholder":"Select number of travellers","rules":{"required":true}},"partial":false,"hasDynamicProps":false,"statementId":"travellersSelect"},"hint":"How many people are travelling?"},"partial":false,"hasDynamicProps":false,"statementId":"fcTravellers"},{"type":"element","typeName":"FormControl","props":{"label":"Trip Type","input":{"type":"element","typeName":"OptionCards","props":{"name":"trip-type","type":"single","items":[{"type":"element","typeName":"OptionCard","props":{"value":"adventure","title":"Adventure","subtitle":"Hiking, trekking & outdoor activities","topContent":{"type":"element","typeName":"Icon","props":{"name":"mountain","category":"nature"},"partial":false,"hasDynamicProps":false,"statementId":"oc1icon"}},"partial":false,"hasDynamicProps":false,"statementId":"oc1"},{"type":"element","typeName":"OptionCard","props":{"value":"beach","title":"Beach & Relaxation","subtitle":"Sun, sand & leisure","topContent":{"type":"element","typeName":"Icon","props":{"name":"waves","category":"nature"},"partial":false,"hasDynamicProps":false,"statementId":"oc2icon"}},"partial":false,"hasDynamicProps":false,"statementId":"oc2"},{"type":"element","typeName":"OptionCard","props":{"value":"cultural","title":"Cultural & Historical","subtitle":"Museums, heritage & local experiences","topContent":{"type":"element","typeName":"Icon","props":{"name":"landmark","category":"buildings"},"partial":false,"hasDynamicProps":false,"statementId":"oc3icon"}},"partial":false,"hasDynamicProps":false,"statementId":"oc3"},{"type":"element","typeName":"OptionCard","props":{"value":"city","title":"City Break","subtitle":"Urban exploration & nightlife","topContent":{"type":"element","typeName":"Icon","props":{"name":"building-2","category":"buildings"},"partial":false,"hasDynamicProps":false,"statementId":"oc4icon"}},"partial":false,"hasDynamicProps":false,"statementId":"oc4"}],"rules":{"required":true}},"partial":false,"hasDynamicProps":false,"statementId":"tripTypeCards"},"hint":"What kind of trip are you planning?"},"partial":false,"hasDynamicProps":false,"statementId":"fcTripType"},{"type":"element","typeName":"FormControl","props":{"label":"Estimated Budget (per person)","input":{"type":"element","typeName":"Slider","props":{"name":"budget","variant":"discrete","min":500,"max":10000,"step":500,"defaultValue":[2500],"label":"Budget (USD)"},"partial":false,"hasDynamicProps":false,"statementId":"budgetSlider"},"hint":"Drag to set your per-person budget in USD"},"partial":false,"hasDynamicProps":false,"statementId":"fcBudget"},{"type":"element","typeName":"FormControl","props":{"label":"Accommodation Preference","input":{"type":"element","typeName":"Chips","props":{"name":"accommodation","type":"multiple","items":[{"type":"element","typeName":"ChipItem","props":{"value":"hotel","label":"Hotel","icon":{"type":"element","typeName":"Icon","props":{"name":"building","category":"buildings"},"partial":false,"hasDynamicProps":false,"statementId":"ch1icon"}},"partial":false,"hasDynamicProps":false,"statementId":"ch1"},{"type":"element","typeName":"ChipItem","props":{"value":"resort","label":"Resort","icon":{"type":"element","typeName":"Icon","props":{"name":"umbrella","category":"travel"},"partial":false,"hasDynamicProps":false,"statementId":"ch2icon"}},"partial":false,"hasDynamicProps":false,"statementId":"ch2"},{"type":"element","typeName":"ChipItem","props":{"value":"airbnb","label":"Vacation Rental","icon":{"type":"element","typeName":"Icon","props":{"name":"home","category":"home"},"partial":false,"hasDynamicProps":false,"statementId":"ch3icon"}},"partial":false,"hasDynamicProps":false,"statementId":"ch3"},{"type":"element","typeName":"ChipItem","props":{"value":"camping","label":"Camping / Glamping","icon":{"type":"element","typeName":"Icon","props":{"name":"tent","category":"nature"},"partial":false,"hasDynamicProps":false,"statementId":"ch4icon"}},"partial":false,"hasDynamicProps":false,"statementId":"ch4"}],"rules":{},"defaultValue":["hotel"]},"partial":false,"hasDynamicProps":false,"statementId":"stayChips"},"hint":"Select all that apply"},"partial":false,"hasDynamicProps":false,"statementId":"fcStay"},{"type":"element","typeName":"FormControl","props":{"label":"Special Requests or Notes","input":{"type":"element","typeName":"TextArea","props":{"name":"special-requests","placeholder":"e.g. vegetarian meals, wheelchair access, honeymoon surprise...","rows":4},"partial":false,"hasDynamicProps":false,"statementId":"notesArea"},"hint":"Any dietary needs, accessibility requirements, or specific interests?"},"partial":false,"hasDynamicProps":false,"statementId":"fcNotes"}]},"partial":false,"hasDynamicProps":false,"statementId":"form"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":50,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"chat-example-4","schema":"chat","input":"Example 4 — Recommendations with images and links (same pattern for products, movies, restaurants, articles):\n\nroot = Card([header, intro, posters, linksHeader, trackCards], [src1, src2])\nheader = CardHeader(\"Retro-Energetic Indian Tracks\", \"New Hindi releases with 80s/90s dance DNA\")\nintro = TextContent(\"These recent tracks channel retro synths and 90s party percussion [1][2]. Tap a poster to explore a track, or use its button to watch the exact official video.\")\nposters = VisualCardBlock([v1, v2], \"grid\", true, { type: \"continue_conversation\", context: \"Tell me more about this track and its retro influences\" })\nv1 = VisualCardItem(v1body, \"tamma\", \"https://i.ytimg.com/vi/PQHeOb0Q9oo/hq720.jpg\", v1tag, \"Tamma Tamma video poster\")\nv1body = BoldText(\"text\", \"Tamma Tamma\", \"Dhurandhar · Ranveer Singh\")\nv1tag = Tag(\"80s Electro-Disco\", v1icon, \"sm\", \"success\")\nv1icon = Icon(\"disc-3\", \"multimedia\")\nv2 = VisualCardItem(v2body, \"sajan\", \"https://i.ytimg.com/vi/mLz-qBiRnl8/hq720.jpg\", v2tag, \"Sajan Re video poster\")\nv2body = BoldText(\"text\", \"Sajan Re\", \"Nora Fatehi · Badshah\")\nv2tag = Tag(\"Synthwave Electro\", v2icon, \"sm\", \"info\")\nv2icon = Icon(\"music\", \"multimedia\")\nlinksHeader = InlineHeader(\"Watch the Exact Videos\", \"Each button opens that track's official video\")\ntrackCards = CompositeCardBlock([t1, t2], \"grid\", true)\nt1 = CompositeCardItem(\"t-tamma\", t1h, [t1body, t1tags], { button: t1btn })\nt1h = IconText(t1icon, \"neutral\", \"m\", \"Tamma Tamma\", \"Bappi Lahiri classic, electro-funk rework\", true, \"horizontal\")\nt1icon = Icon(\"disc-3\", \"multimedia\")\nt1body = Text(\"text\", \"Heavy analog synths and brass over a fast disco groove.\", \"Retro Electro-Disco\")\nt1tags = TagBlock([\"2026 Film Hit\", \"Peak Party\"])\nt1btn = Button(\"Watch on YouTube\", { type: \"open_url\", url: \"https://www.youtube.com/watch?v=PQHeOb0Q9oo\" }, \"primary\")\nt2 = CompositeCardItem(\"t-sajan\", t2h, [t2body, t2tags], { button: t2btn })\nt2h = IconText(t2icon, \"neutral\", \"m\", \"Sajan Re\", \"Nora Fatehi · Badshah · Sanjoy\", true, \"horizontal\")\nt2icon = Icon(\"music\", \"multimedia\")\nt2body = Text(\"text\", \"Synthwave melody hooks over a modern club beat.\", \"Retro-Fusion Electro\")\nt2tags = TagBlock([\"Club Anthem\", \"Nora x Badshah\"])\nt2btn = Button(\"Watch on YouTube\", { type: \"open_url\", url: \"https://www.youtube.com/watch?v=COUSCnFazzc\" }, \"primary\")\nsrc1 = { title: \"Tamma Tamma (Full Video) - Dhurandhar\", sourceName: \"YouTube (T-Series)\", url: \"https://www.youtube.com/watch?v=PQHeOb0Q9oo\" }\nsrc2 = { title: \"Sajan Re (Official Music Video)\", sourceName: \"YouTube (T-Series)\", url: \"https://www.youtube.com/watch?v=COUSCnFazzc\" }","checkpoints":[{"at":218,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Retro-Ene"},"partial":true,"hasDynamicProps":false,"statementId":"header"}],"sources":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["intro","posters","linksHeader","trackCards","src1","src2"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":436,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Retro-Energetic Indian Tracks","subtitle":"New Hindi releases with 80s/90s dance DNA"},"partial":true,"hasDynamicProps":false,"statementId":"header"},{"type":"element","typeName":"TextContent","props":{"text":"These recent tracks channel retro synths and 90s party percussion [1][2]. Tap a poster to explore a track, or use its button to w"},"partial":true,"hasDynamicProps":false,"statementId":"intro"}],"sources":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["posters","linksHeader","trackCards","src1","src2"],"orphaned":[],"statementCount":3,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":654,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Retro-Energetic Indian Tracks","subtitle":"New Hindi releases with 80s/90s dance DNA"},"partial":true,"hasDynamicProps":false,"statementId":"header"},{"type":"element","typeName":"TextContent","props":{"text":"These recent tracks channel retro synths and 90s party percussion [1][2]. Tap a poster to explore a track, or use its button to watch the exact official video."},"partial":true,"hasDynamicProps":false,"statementId":"intro"},{"type":"element","typeName":"VisualCardBlock","props":{"items":[],"layout":"grid","responsive":true,"action":{"type":"continue_conversation","context":"Tell me more about this track and its retro influences"}},"partial":true,"hasDynamicProps":false,"statementId":"posters"}],"sources":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["v1body","v2","linksHeader","trackCards","src1","src2"],"orphaned":[],"statementCount":5,"errors":[{"code":"null-required","component":"VisualCardItem","path":"/body","message":"required field \"/body\" cannot be null — signature: VisualCardItem(body*: BoldText, id: string, bgImageSrc: string, tag: Tag, bgImageAlt: string)","statementId":"v1"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":872,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Retro-Energetic Indian Tracks","subtitle":"New Hindi releases with 80s/90s dance DNA"},"partial":false,"hasDynamicProps":false,"statementId":"header"},{"type":"element","typeName":"TextContent","props":{"text":"These recent tracks channel retro synths and 90s party percussion [1][2]. Tap a poster to explore a track, or use its button to watch the exact official video."},"partial":false,"hasDynamicProps":false,"statementId":"intro"},{"type":"element","typeName":"VisualCardBlock","props":{"items":[{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Tamma Tamma","subtext":"Dhurandhar · Ranveer Singh","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"v1body"},"id":"tamma","bgImageSrc":"https://i.ytimg.com/vi/PQHeOb0Q9oo/hq720.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"80s Electro-Disco","icon":null,"size":"sm","variant":"success"},"partial":false,"hasDynamicProps":false,"statementId":"v1tag"},"bgImageAlt":"Tamma Tamma video poster"},"partial":false,"hasDynamicProps":false,"statementId":"v1"}],"layout":"grid","responsive":true,"action":{"type":"continue_conversation","context":"Tell me more about this track and its retro influences"}},"partial":false,"hasDynamicProps":false,"statementId":"posters"}],"sources":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["v1icon","v2","linksHeader","trackCards","src1","src2"],"orphaned":[],"statementCount":7,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":1090,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Retro-Energetic Indian Tracks","subtitle":"New Hindi releases with 80s/90s dance DNA"},"partial":false,"hasDynamicProps":false,"statementId":"header"},{"type":"element","typeName":"TextContent","props":{"text":"These recent tracks channel retro synths and 90s party percussion [1][2]. Tap a poster to explore a track, or use its button to watch the exact official video."},"partial":false,"hasDynamicProps":false,"statementId":"intro"},{"type":"element","typeName":"VisualCardBlock","props":{"items":[{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Tamma Tamma","subtext":"Dhurandhar · Ranveer Singh","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"v1body"},"id":"tamma","bgImageSrc":"https://i.ytimg.com/vi/PQHeOb0Q9oo/hq720.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"80s Electro-Disco","icon":{"type":"element","typeName":"Icon","props":{"name":"disc-3","category":"multimedia"},"partial":false,"hasDynamicProps":false,"statementId":"v1icon"},"size":"sm","variant":"success"},"partial":false,"hasDynamicProps":false,"statementId":"v1tag"},"bgImageAlt":"Tamma Tamma video poster"},"partial":false,"hasDynamicProps":false,"statementId":"v1"},{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Sajan Re","subtext":"Nora Fatehi · Badshah","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"v2body"},"id":"sajan","bgImageSrc":"https://i.ytimg.com/vi/mLz-qBiRnl8/hq720.jpg","tag":null,"bgImageAlt":"Sajan Re video poster"},"partial":false,"hasDynamicProps":false,"statementId":"v2"}],"layout":"grid","responsive":true,"action":{"type":"continue_conversation","context":"Tell me more about this track and its retro influences"}},"partial":false,"hasDynamicProps":false,"statementId":"posters"}],"sources":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["v2tag","linksHeader","trackCards","src1","src2"],"orphaned":[],"statementCount":10,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":1308,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Retro-Energetic Indian Tracks","subtitle":"New Hindi releases with 80s/90s dance DNA"},"partial":false,"hasDynamicProps":false,"statementId":"header"},{"type":"element","typeName":"TextContent","props":{"text":"These recent tracks channel retro synths and 90s party percussion [1][2]. Tap a poster to explore a track, or use its button to watch the exact official video."},"partial":false,"hasDynamicProps":false,"statementId":"intro"},{"type":"element","typeName":"VisualCardBlock","props":{"items":[{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Tamma Tamma","subtext":"Dhurandhar · Ranveer Singh","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"v1body"},"id":"tamma","bgImageSrc":"https://i.ytimg.com/vi/PQHeOb0Q9oo/hq720.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"80s Electro-Disco","icon":{"type":"element","typeName":"Icon","props":{"name":"disc-3","category":"multimedia"},"partial":false,"hasDynamicProps":false,"statementId":"v1icon"},"size":"sm","variant":"success"},"partial":false,"hasDynamicProps":false,"statementId":"v1tag"},"bgImageAlt":"Tamma Tamma video poster"},"partial":false,"hasDynamicProps":false,"statementId":"v1"},{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Sajan Re","subtext":"Nora Fatehi · Badshah","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"v2body"},"id":"sajan","bgImageSrc":"https://i.ytimg.com/vi/mLz-qBiRnl8/hq720.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Synthwave Electro","icon":{"type":"element","typeName":"Icon","props":{"name":"music","category":"multimedia"},"partial":false,"hasDynamicProps":false,"statementId":"v2icon"},"size":"sm","variant":"info"},"partial":false,"hasDynamicProps":false,"statementId":"v2tag"},"bgImageAlt":"Sajan Re video poster"},"partial":false,"hasDynamicProps":false,"statementId":"v2"}],"layout":"grid","responsive":true,"action":{"type":"continue_conversation","context":"Tell me more about this track and its retro influences"}},"partial":false,"hasDynamicProps":false,"statementId":"posters"},{"type":"element","typeName":"InlineHeader","props":{"heading":"Watch the Exact Videos","description":"Each button opens that track's official video"},"partial":false,"hasDynamicProps":false,"statementId":"linksHeader"}],"sources":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["CompositeCard","src1","src2"],"orphaned":[],"statementCount":14,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":1526,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Retro-Energetic Indian Tracks","subtitle":"New Hindi releases with 80s/90s dance DNA"},"partial":true,"hasDynamicProps":false,"statementId":"header"},{"type":"element","typeName":"TextContent","props":{"text":"These recent tracks channel retro synths and 90s party percussion [1][2]. Tap a poster to explore a track, or use its button to watch the exact official video."},"partial":true,"hasDynamicProps":false,"statementId":"intro"},{"type":"element","typeName":"VisualCardBlock","props":{"items":[{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Tamma Tamma","subtext":"Dhurandhar · Ranveer Singh","subtextVariant":"text","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"v1body"},"id":"tamma","bgImageSrc":"https://i.ytimg.com/vi/PQHeOb0Q9oo/hq720.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"80s Electro-Disco","icon":{"type":"element","typeName":"Icon","props":{"name":"disc-3","category":"multimedia"},"partial":true,"hasDynamicProps":false,"statementId":"v1icon"},"size":"sm","variant":"success"},"partial":true,"hasDynamicProps":false,"statementId":"v1tag"},"bgImageAlt":"Tamma Tamma video poster"},"partial":true,"hasDynamicProps":false,"statementId":"v1"},{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Sajan Re","subtext":"Nora Fatehi · Badshah","subtextVariant":"text","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"v2body"},"id":"sajan","bgImageSrc":"https://i.ytimg.com/vi/mLz-qBiRnl8/hq720.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Synthwave Electro","icon":{"type":"element","typeName":"Icon","props":{"name":"music","category":"multimedia"},"partial":true,"hasDynamicProps":false,"statementId":"v2icon"},"size":"sm","variant":"info"},"partial":true,"hasDynamicProps":false,"statementId":"v2tag"},"bgImageAlt":"Sajan Re video poster"},"partial":true,"hasDynamicProps":false,"statementId":"v2"}],"layout":"grid","responsive":true,"action":{"type":"continue_conversation","context":"Tell me more about this track and its retro influences"}},"partial":true,"hasDynamicProps":false,"statementId":"posters"},{"type":"element","typeName":"InlineHeader","props":{"heading":"Watch the Exact Videos","description":"Each button opens that track's official video"},"partial":true,"hasDynamicProps":false,"statementId":"linksHeader"},{"type":"element","typeName":"CompositeCardBlock","props":{"items":[{"type":"element","typeName":"CompositeCardItem","props":{"id":"t-tamma","header":null,"body":[],"footer":{"button":null}},"partial":true,"hasDynamicProps":false,"statementId":"t1"}],"layout":"grid","responsive":true},"partial":true,"hasDynamicProps":false,"statementId":"trackCards"}],"sources":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["t1icon","t1body","t1tags","t1btn","t2","src1","src2"],"orphaned":[],"statementCount":16,"errors":[{"code":"null-required","component":"IconText","path":"/icon","message":"required field \"/icon\" cannot be null — signature: IconText(icon*: Icon, iconVariant*: \"neutral\"|\"info\"|\"success\"|\"warning\"|\"danger\"|\"inverted\"|\"filled\"|\"soft\", iconSize*: \"xs\"|\"s\"|\"m\"|\"l\"|\"xl\"|\"sm\"|\"md\"|\"lg\", title*: string, subtitle: string, bold*: boolean, layout*: \"horizontal\"|\"vertical\")","statementId":"t1h"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":1744,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Retro-Energetic Indian Tracks","subtitle":"New Hindi releases with 80s/90s dance DNA"},"partial":true,"hasDynamicProps":false,"statementId":"header"},{"type":"element","typeName":"TextContent","props":{"text":"These recent tracks channel retro synths and 90s party percussion [1][2]. Tap a poster to explore a track, or use its button to watch the exact official video."},"partial":true,"hasDynamicProps":false,"statementId":"intro"},{"type":"element","typeName":"VisualCardBlock","props":{"items":[{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Tamma Tamma","subtext":"Dhurandhar · Ranveer Singh","subtextVariant":"text","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"v1body"},"id":"tamma","bgImageSrc":"https://i.ytimg.com/vi/PQHeOb0Q9oo/hq720.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"80s Electro-Disco","icon":{"type":"element","typeName":"Icon","props":{"name":"disc-3","category":"multimedia"},"partial":true,"hasDynamicProps":false,"statementId":"v1icon"},"size":"sm","variant":"success"},"partial":true,"hasDynamicProps":false,"statementId":"v1tag"},"bgImageAlt":"Tamma Tamma video poster"},"partial":true,"hasDynamicProps":false,"statementId":"v1"},{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Sajan Re","subtext":"Nora Fatehi · Badshah","subtextVariant":"text","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"v2body"},"id":"sajan","bgImageSrc":"https://i.ytimg.com/vi/mLz-qBiRnl8/hq720.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Synthwave Electro","icon":{"type":"element","typeName":"Icon","props":{"name":"music","category":"multimedia"},"partial":true,"hasDynamicProps":false,"statementId":"v2icon"},"size":"sm","variant":"info"},"partial":true,"hasDynamicProps":false,"statementId":"v2tag"},"bgImageAlt":"Sajan Re video poster"},"partial":true,"hasDynamicProps":false,"statementId":"v2"}],"layout":"grid","responsive":true,"action":{"type":"continue_conversation","context":"Tell me more about this track and its retro influences"}},"partial":true,"hasDynamicProps":false,"statementId":"posters"},{"type":"element","typeName":"InlineHeader","props":{"heading":"Watch the Exact Videos","description":"Each button opens that track's official video"},"partial":true,"hasDynamicProps":false,"statementId":"linksHeader"},{"type":"element","typeName":"CompositeCardBlock","props":{"items":[{"type":"element","typeName":"CompositeCardItem","props":{"id":"t-tamma","header":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"disc-3","category":"multimedia"},"partial":true,"hasDynamicProps":false,"statementId":"t1icon"},"iconVariant":"neutral","iconSize":"m","title":"Tamma Tamma","subtitle":"Bappi Lahiri classic, electro-funk rework","bold":true,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"t1h"},"body":[{"type":"element","typeName":"Text","props":{"variant":"text","value":"Heavy analog synths and brass over a fast disco groove.","subtext":"Retro Electro-Disco","subtextVariant":"text","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"t1body"},{"type":"element","typeName":"TagBlock","props":{"tags":["2026 Film Hit","Peak Party"]},"partial":true,"hasDynamicProps":false,"statementId":"t1tags"}],"footer":{"button":{"type":"element","typeName":"Button","props":{"label":"Wa"},"partial":true,"hasDynamicProps":false,"statementId":"t1btn"}}},"partial":true,"hasDynamicProps":false,"statementId":"t1"}],"layout":"grid","responsive":true},"partial":true,"hasDynamicProps":false,"statementId":"trackCards"}],"sources":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["t2","src1","src2"],"orphaned":[],"statementCount":20,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":1962,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Retro-Energetic Indian Tracks","subtitle":"New Hindi releases with 80s/90s dance DNA"},"partial":true,"hasDynamicProps":false,"statementId":"header"},{"type":"element","typeName":"TextContent","props":{"text":"These recent tracks channel retro synths and 90s party percussion [1][2]. Tap a poster to explore a track, or use its button to watch the exact official video."},"partial":true,"hasDynamicProps":false,"statementId":"intro"},{"type":"element","typeName":"VisualCardBlock","props":{"items":[{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Tamma Tamma","subtext":"Dhurandhar · Ranveer Singh","subtextVariant":"text","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"v1body"},"id":"tamma","bgImageSrc":"https://i.ytimg.com/vi/PQHeOb0Q9oo/hq720.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"80s Electro-Disco","icon":{"type":"element","typeName":"Icon","props":{"name":"disc-3","category":"multimedia"},"partial":true,"hasDynamicProps":false,"statementId":"v1icon"},"size":"sm","variant":"success"},"partial":true,"hasDynamicProps":false,"statementId":"v1tag"},"bgImageAlt":"Tamma Tamma video poster"},"partial":true,"hasDynamicProps":false,"statementId":"v1"},{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Sajan Re","subtext":"Nora Fatehi · Badshah","subtextVariant":"text","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"v2body"},"id":"sajan","bgImageSrc":"https://i.ytimg.com/vi/mLz-qBiRnl8/hq720.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Synthwave Electro","icon":{"type":"element","typeName":"Icon","props":{"name":"music","category":"multimedia"},"partial":true,"hasDynamicProps":false,"statementId":"v2icon"},"size":"sm","variant":"info"},"partial":true,"hasDynamicProps":false,"statementId":"v2tag"},"bgImageAlt":"Sajan Re video poster"},"partial":true,"hasDynamicProps":false,"statementId":"v2"}],"layout":"grid","responsive":true,"action":{"type":"continue_conversation","context":"Tell me more about this track and its retro influences"}},"partial":true,"hasDynamicProps":false,"statementId":"posters"},{"type":"element","typeName":"InlineHeader","props":{"heading":"Watch the Exact Videos","description":"Each button opens that track's official video"},"partial":true,"hasDynamicProps":false,"statementId":"linksHeader"},{"type":"element","typeName":"CompositeCardBlock","props":{"items":[{"type":"element","typeName":"CompositeCardItem","props":{"id":"t-tamma","header":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"disc-3","category":"multimedia"},"partial":true,"hasDynamicProps":false,"statementId":"t1icon"},"iconVariant":"neutral","iconSize":"m","title":"Tamma Tamma","subtitle":"Bappi Lahiri classic, electro-funk rework","bold":true,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"t1h"},"body":[{"type":"element","typeName":"Text","props":{"variant":"text","value":"Heavy analog synths and brass over a fast disco groove.","subtext":"Retro Electro-Disco","subtextVariant":"text","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"t1body"},{"type":"element","typeName":"TagBlock","props":{"tags":["2026 Film Hit","Peak Party"]},"partial":true,"hasDynamicProps":false,"statementId":"t1tags"}],"footer":{"button":{"type":"element","typeName":"Button","props":{"label":"Watch on YouTube","action":{"type":"open_url","url":"https://www.youtube.com/watch?v=PQHeOb0Q9oo"},"variant":"primary"},"partial":true,"hasDynamicProps":false,"statementId":"t1btn"}}},"partial":true,"hasDynamicProps":false,"statementId":"t1"},{"type":"element","typeName":"CompositeCardItem","props":{"id":"t-sajan","header":null,"body":[],"footer":{"button":null}},"partial":true,"hasDynamicProps":false,"statementId":"t2"}],"layout":"grid","responsive":true},"partial":true,"hasDynamicProps":false,"statementId":"trackCards"}],"sources":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["t2icon","t2body","t2tags","t2btn","src1","src2"],"orphaned":[],"statementCount":22,"errors":[{"code":"null-required","component":"IconText","path":"/icon","message":"required field \"/icon\" cannot be null — signature: IconText(icon*: Icon, iconVariant*: \"neutral\"|\"info\"|\"success\"|\"warning\"|\"danger\"|\"inverted\"|\"filled\"|\"soft\", iconSize*: \"xs\"|\"s\"|\"m\"|\"l\"|\"xl\"|\"sm\"|\"md\"|\"lg\", title*: string, subtitle: string, bold*: boolean, layout*: \"horizontal\"|\"vertical\")","statementId":"t2h"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":2180,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Retro-Energetic Indian Tracks","subtitle":"New Hindi releases with 80s/90s dance DNA"},"partial":true,"hasDynamicProps":false,"statementId":"header"},{"type":"element","typeName":"TextContent","props":{"text":"These recent tracks channel retro synths and 90s party percussion [1][2]. Tap a poster to explore a track, or use its button to watch the exact official video."},"partial":true,"hasDynamicProps":false,"statementId":"intro"},{"type":"element","typeName":"VisualCardBlock","props":{"items":[{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Tamma Tamma","subtext":"Dhurandhar · Ranveer Singh","subtextVariant":"text","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"v1body"},"id":"tamma","bgImageSrc":"https://i.ytimg.com/vi/PQHeOb0Q9oo/hq720.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"80s Electro-Disco","icon":{"type":"element","typeName":"Icon","props":{"name":"disc-3","category":"multimedia"},"partial":true,"hasDynamicProps":false,"statementId":"v1icon"},"size":"sm","variant":"success"},"partial":true,"hasDynamicProps":false,"statementId":"v1tag"},"bgImageAlt":"Tamma Tamma video poster"},"partial":true,"hasDynamicProps":false,"statementId":"v1"},{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Sajan Re","subtext":"Nora Fatehi · Badshah","subtextVariant":"text","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"v2body"},"id":"sajan","bgImageSrc":"https://i.ytimg.com/vi/mLz-qBiRnl8/hq720.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Synthwave Electro","icon":{"type":"element","typeName":"Icon","props":{"name":"music","category":"multimedia"},"partial":true,"hasDynamicProps":false,"statementId":"v2icon"},"size":"sm","variant":"info"},"partial":true,"hasDynamicProps":false,"statementId":"v2tag"},"bgImageAlt":"Sajan Re video poster"},"partial":true,"hasDynamicProps":false,"statementId":"v2"}],"layout":"grid","responsive":true,"action":{"type":"continue_conversation","context":"Tell me more about this track and its retro influences"}},"partial":true,"hasDynamicProps":false,"statementId":"posters"},{"type":"element","typeName":"InlineHeader","props":{"heading":"Watch the Exact Videos","description":"Each button opens that track's official video"},"partial":true,"hasDynamicProps":false,"statementId":"linksHeader"},{"type":"element","typeName":"CompositeCardBlock","props":{"items":[{"type":"element","typeName":"CompositeCardItem","props":{"id":"t-tamma","header":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"disc-3","category":"multimedia"},"partial":true,"hasDynamicProps":false,"statementId":"t1icon"},"iconVariant":"neutral","iconSize":"m","title":"Tamma Tamma","subtitle":"Bappi Lahiri classic, electro-funk rework","bold":true,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"t1h"},"body":[{"type":"element","typeName":"Text","props":{"variant":"text","value":"Heavy analog synths and brass over a fast disco groove.","subtext":"Retro Electro-Disco","subtextVariant":"text","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"t1body"},{"type":"element","typeName":"TagBlock","props":{"tags":["2026 Film Hit","Peak Party"]},"partial":true,"hasDynamicProps":false,"statementId":"t1tags"}],"footer":{"button":{"type":"element","typeName":"Button","props":{"label":"Watch on YouTube","action":{"type":"open_url","url":"https://www.youtube.com/watch?v=PQHeOb0Q9oo"},"variant":"primary"},"partial":true,"hasDynamicProps":false,"statementId":"t1btn"}}},"partial":true,"hasDynamicProps":false,"statementId":"t1"},{"type":"element","typeName":"CompositeCardItem","props":{"id":"t-sajan","header":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"music","category":"multimedia"},"partial":true,"hasDynamicProps":false,"statementId":"t2icon"},"iconVariant":"neutral","iconSize":"m","title":"Sajan Re","subtitle":"Nora Fatehi · Badshah · Sanjoy","bold":true,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"t2h"},"body":[{"type":"element","typeName":"Text","props":{"variant":"text","value":"Synthwave melody hooks over a modern club beat.","subtext":"Retro-Fusion Electro","subtextVariant":"text","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"t2body"},{"type":"element","typeName":"TagBlock","props":{"tags":[]},"partial":true,"hasDynamicProps":false,"statementId":"t2tags"}],"footer":{"button":null}},"partial":true,"hasDynamicProps":false,"statementId":"t2"}],"layout":"grid","responsive":true},"partial":true,"hasDynamicProps":false,"statementId":"trackCards"}],"sources":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["t2btn","src1","src2"],"orphaned":[],"statementCount":25,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":2398,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Retro-Energetic Indian Tracks","subtitle":"New Hindi releases with 80s/90s dance DNA"},"partial":true,"hasDynamicProps":false,"statementId":"header"},{"type":"element","typeName":"TextContent","props":{"text":"These recent tracks channel retro synths and 90s party percussion [1][2]. Tap a poster to explore a track, or use its button to watch the exact official video."},"partial":true,"hasDynamicProps":false,"statementId":"intro"},{"type":"element","typeName":"VisualCardBlock","props":{"items":[{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Tamma Tamma","subtext":"Dhurandhar · Ranveer Singh","subtextVariant":"text","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"v1body"},"id":"tamma","bgImageSrc":"https://i.ytimg.com/vi/PQHeOb0Q9oo/hq720.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"80s Electro-Disco","icon":{"type":"element","typeName":"Icon","props":{"name":"disc-3","category":"multimedia"},"partial":true,"hasDynamicProps":false,"statementId":"v1icon"},"size":"sm","variant":"success"},"partial":true,"hasDynamicProps":false,"statementId":"v1tag"},"bgImageAlt":"Tamma Tamma video poster"},"partial":true,"hasDynamicProps":false,"statementId":"v1"},{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Sajan Re","subtext":"Nora Fatehi · Badshah","subtextVariant":"text","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"v2body"},"id":"sajan","bgImageSrc":"https://i.ytimg.com/vi/mLz-qBiRnl8/hq720.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Synthwave Electro","icon":{"type":"element","typeName":"Icon","props":{"name":"music","category":"multimedia"},"partial":true,"hasDynamicProps":false,"statementId":"v2icon"},"size":"sm","variant":"info"},"partial":true,"hasDynamicProps":false,"statementId":"v2tag"},"bgImageAlt":"Sajan Re video poster"},"partial":true,"hasDynamicProps":false,"statementId":"v2"}],"layout":"grid","responsive":true,"action":{"type":"continue_conversation","context":"Tell me more about this track and its retro influences"}},"partial":true,"hasDynamicProps":false,"statementId":"posters"},{"type":"element","typeName":"InlineHeader","props":{"heading":"Watch the Exact Videos","description":"Each button opens that track's official video"},"partial":true,"hasDynamicProps":false,"statementId":"linksHeader"},{"type":"element","typeName":"CompositeCardBlock","props":{"items":[{"type":"element","typeName":"CompositeCardItem","props":{"id":"t-tamma","header":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"disc-3","category":"multimedia"},"partial":true,"hasDynamicProps":false,"statementId":"t1icon"},"iconVariant":"neutral","iconSize":"m","title":"Tamma Tamma","subtitle":"Bappi Lahiri classic, electro-funk rework","bold":true,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"t1h"},"body":[{"type":"element","typeName":"Text","props":{"variant":"text","value":"Heavy analog synths and brass over a fast disco groove.","subtext":"Retro Electro-Disco","subtextVariant":"text","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"t1body"},{"type":"element","typeName":"TagBlock","props":{"tags":["2026 Film Hit","Peak Party"]},"partial":true,"hasDynamicProps":false,"statementId":"t1tags"}],"footer":{"button":{"type":"element","typeName":"Button","props":{"label":"Watch on YouTube","action":{"type":"open_url","url":"https://www.youtube.com/watch?v=PQHeOb0Q9oo"},"variant":"primary"},"partial":true,"hasDynamicProps":false,"statementId":"t1btn"}}},"partial":true,"hasDynamicProps":false,"statementId":"t1"},{"type":"element","typeName":"CompositeCardItem","props":{"id":"t-sajan","header":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"music","category":"multimedia"},"partial":true,"hasDynamicProps":false,"statementId":"t2icon"},"iconVariant":"neutral","iconSize":"m","title":"Sajan Re","subtitle":"Nora Fatehi · Badshah · Sanjoy","bold":true,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"t2h"},"body":[{"type":"element","typeName":"Text","props":{"variant":"text","value":"Synthwave melody hooks over a modern club beat.","subtext":"Retro-Fusion Electro","subtextVariant":"text","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"t2body"},{"type":"element","typeName":"TagBlock","props":{"tags":["Club Anthem","Nora x Badshah"]},"partial":true,"hasDynamicProps":false,"statementId":"t2tags"}],"footer":{"button":{"type":"element","typeName":"Button","props":{"label":"Watch on YouTube","action":{"type":"open_url","url":"https://www.youtube.com/watch?v=COUSCnFazzc"},"variant":"primary"},"partial":true,"hasDynamicProps":false,"statementId":"t2btn"}}},"partial":true,"hasDynamicProps":false,"statementId":"t2"}],"layout":"grid","responsive":true},"partial":true,"hasDynamicProps":false,"statementId":"trackCards"}],"sources":[{"title":"Tamma Tamma (Full Video) - Dhurandhar","sourceN":null}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["src2"],"orphaned":[],"statementCount":27,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":2615,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Retro-Energetic Indian Tracks","subtitle":"New Hindi releases with 80s/90s dance DNA"},"partial":false,"hasDynamicProps":false,"statementId":"header"},{"type":"element","typeName":"TextContent","props":{"text":"These recent tracks channel retro synths and 90s party percussion [1][2]. Tap a poster to explore a track, or use its button to watch the exact official video."},"partial":false,"hasDynamicProps":false,"statementId":"intro"},{"type":"element","typeName":"VisualCardBlock","props":{"items":[{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Tamma Tamma","subtext":"Dhurandhar · Ranveer Singh","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"v1body"},"id":"tamma","bgImageSrc":"https://i.ytimg.com/vi/PQHeOb0Q9oo/hq720.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"80s Electro-Disco","icon":{"type":"element","typeName":"Icon","props":{"name":"disc-3","category":"multimedia"},"partial":false,"hasDynamicProps":false,"statementId":"v1icon"},"size":"sm","variant":"success"},"partial":false,"hasDynamicProps":false,"statementId":"v1tag"},"bgImageAlt":"Tamma Tamma video poster"},"partial":false,"hasDynamicProps":false,"statementId":"v1"},{"type":"element","typeName":"VisualCardItem","props":{"body":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"Sajan Re","subtext":"Nora Fatehi · Badshah","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"v2body"},"id":"sajan","bgImageSrc":"https://i.ytimg.com/vi/mLz-qBiRnl8/hq720.jpg","tag":{"type":"element","typeName":"Tag","props":{"text":"Synthwave Electro","icon":{"type":"element","typeName":"Icon","props":{"name":"music","category":"multimedia"},"partial":false,"hasDynamicProps":false,"statementId":"v2icon"},"size":"sm","variant":"info"},"partial":false,"hasDynamicProps":false,"statementId":"v2tag"},"bgImageAlt":"Sajan Re video poster"},"partial":false,"hasDynamicProps":false,"statementId":"v2"}],"layout":"grid","responsive":true,"action":{"type":"continue_conversation","context":"Tell me more about this track and its retro influences"}},"partial":false,"hasDynamicProps":false,"statementId":"posters"},{"type":"element","typeName":"InlineHeader","props":{"heading":"Watch the Exact Videos","description":"Each button opens that track's official video"},"partial":false,"hasDynamicProps":false,"statementId":"linksHeader"},{"type":"element","typeName":"CompositeCardBlock","props":{"items":[{"type":"element","typeName":"CompositeCardItem","props":{"id":"t-tamma","header":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"disc-3","category":"multimedia"},"partial":false,"hasDynamicProps":false,"statementId":"t1icon"},"iconVariant":"neutral","iconSize":"m","title":"Tamma Tamma","subtitle":"Bappi Lahiri classic, electro-funk rework","bold":true,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"t1h"},"body":[{"type":"element","typeName":"Text","props":{"variant":"text","value":"Heavy analog synths and brass over a fast disco groove.","subtext":"Retro Electro-Disco","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"t1body"},{"type":"element","typeName":"TagBlock","props":{"tags":["2026 Film Hit","Peak Party"]},"partial":false,"hasDynamicProps":false,"statementId":"t1tags"}],"footer":{"button":{"type":"element","typeName":"Button","props":{"label":"Watch on YouTube","action":{"type":"open_url","url":"https://www.youtube.com/watch?v=PQHeOb0Q9oo"},"variant":"primary"},"partial":false,"hasDynamicProps":false,"statementId":"t1btn"}}},"partial":false,"hasDynamicProps":false,"statementId":"t1"},{"type":"element","typeName":"CompositeCardItem","props":{"id":"t-sajan","header":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"music","category":"multimedia"},"partial":false,"hasDynamicProps":false,"statementId":"t2icon"},"iconVariant":"neutral","iconSize":"m","title":"Sajan Re","subtitle":"Nora Fatehi · Badshah · Sanjoy","bold":true,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"t2h"},"body":[{"type":"element","typeName":"Text","props":{"variant":"text","value":"Synthwave melody hooks over a modern club beat.","subtext":"Retro-Fusion Electro","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"t2body"},{"type":"element","typeName":"TagBlock","props":{"tags":["Club Anthem","Nora x Badshah"]},"partial":false,"hasDynamicProps":false,"statementId":"t2tags"}],"footer":{"button":{"type":"element","typeName":"Button","props":{"label":"Watch on YouTube","action":{"type":"open_url","url":"https://www.youtube.com/watch?v=COUSCnFazzc"},"variant":"primary"},"partial":false,"hasDynamicProps":false,"statementId":"t2btn"}}},"partial":false,"hasDynamicProps":false,"statementId":"t2"}],"layout":"grid","responsive":true},"partial":false,"hasDynamicProps":false,"statementId":"trackCards"}],"sources":[{"title":"Tamma Tamma (Full Video) - Dhurandhar","sourceName":"YouTube (T-Series)","url":"https://www.youtube.com/watch?v=PQHeOb0Q9oo"},{"title":"Sajan Re (Official Music Video)","sourceName":"YouTube (T-Series)","url":"https://www.youtube.com/watch?v=COUSCnFazzc"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":28,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"chat-example-5","schema":"chat","input":"Example 5 — Table with follow-ups:\n\nroot = Card([title, tbl, followUps])\ntitle = TextContent(\"Top Languages\", \"large-heavy\")\ntbl = Table([Col(\"Language\", langs), Col(\"Users (M)\", users), Col(\"Year\", years)])\nlangs = [\"Python\", \"JavaScript\", \"Java\"]\nusers = [15.7, 14.2, 12.1]\nyears = [1991, 1995, 1995]\nfollowUps = FollowUpBlock([fu1, fu2])\nfu1 = FollowUpItem(\"Tell me more about Python\")\nfu2 = FollowUpItem(\"Show me a JavaScript comparison\")","checkpoints":[{"at":37,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":74,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["title","tbl","followUps"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":111,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"Top Languages","size":""},"partial":true,"hasDynamicProps":false,"statementId":"title"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["tbl","followUps"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":148,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"Top Languages","size":"large-heavy"},"partial":true,"hasDynamicProps":false,"statementId":"title"},{"type":"element","typeName":"Table","props":{"columns":[]},"partial":true,"hasDynamicProps":false,"statementId":"tbl"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["followUps"],"orphaned":[],"statementCount":3,"errors":[{"code":"missing-required","component":"Col","path":"/data","message":"missing required field \"/data\" — signature: Col(label*: string, data*, type: \"string\"|\"number\"|\"action\")","statementId":"tbl"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":185,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"Top Languages","size":"large-heavy"},"partial":true,"hasDynamicProps":false,"statementId":"title"},{"type":"element","typeName":"Table","props":{"columns":[]},"partial":true,"hasDynamicProps":false,"statementId":"tbl"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["langs","users","followUps"],"orphaned":[],"statementCount":3,"errors":[{"code":"null-required","component":"Col","path":"/data","message":"required field \"/data\" cannot be null — signature: Col(label*: string, data*, type: \"string\"|\"number\"|\"action\")","statementId":"tbl"},{"code":"null-required","component":"Col","path":"/data","message":"required field \"/data\" cannot be null — signature: Col(label*: string, data*, type: \"string\"|\"number\"|\"action\")","statementId":"tbl"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":222,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"Top Languages","size":"large-heavy"},"partial":true,"hasDynamicProps":false,"statementId":"title"},{"type":"element","typeName":"Table","props":{"columns":[{"type":"element","typeName":"Col","props":{"label":"Language","data":["Pyth"]},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"tbl"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["users","years","followUps"],"orphaned":[],"statementCount":4,"errors":[{"code":"null-required","component":"Col","path":"/data","message":"required field \"/data\" cannot be null — signature: Col(label*: string, data*, type: \"string\"|\"number\"|\"action\")","statementId":"tbl"},{"code":"null-required","component":"Col","path":"/data","message":"required field \"/data\" cannot be null — signature: Col(label*: string, data*, type: \"string\"|\"number\"|\"action\")","statementId":"tbl"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":259,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"Top Languages","size":"large-heavy"},"partial":true,"hasDynamicProps":false,"statementId":"title"},{"type":"element","typeName":"Table","props":{"columns":[{"type":"element","typeName":"Col","props":{"label":"Language","data":["Python","JavaScript","Java"]},"partial":true,"hasDynamicProps":false},{"type":"element","typeName":"Col","props":{"label":"Users (M)","data":[1]},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"tbl"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["years","followUps"],"orphaned":[],"statementCount":5,"errors":[{"code":"null-required","component":"Col","path":"/data","message":"required field \"/data\" cannot be null — signature: Col(label*: string, data*, type: \"string\"|\"number\"|\"action\")","statementId":"tbl"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":296,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"Top Languages","size":"large-heavy"},"partial":true,"hasDynamicProps":false,"statementId":"title"},{"type":"element","typeName":"Table","props":{"columns":[{"type":"element","typeName":"Col","props":{"label":"Language","data":["Python","JavaScript","Java"]},"partial":true,"hasDynamicProps":false},{"type":"element","typeName":"Col","props":{"label":"Users (M)","data":[15.7,14.2,12.1]},"partial":true,"hasDynamicProps":false},{"type":"element","typeName":"Col","props":{"label":"Year","data":[1991,1995]},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"tbl"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["followUps"],"orphaned":[],"statementCount":6,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":333,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"Top Languages","size":"large-heavy"},"partial":true,"hasDynamicProps":false,"statementId":"title"},{"type":"element","typeName":"Table","props":{"columns":[{"type":"element","typeName":"Col","props":{"label":"Language","data":["Python","JavaScript","Java"]},"partial":true,"hasDynamicProps":false},{"type":"element","typeName":"Col","props":{"label":"Users (M)","data":[15.7,14.2,12.1]},"partial":true,"hasDynamicProps":false},{"type":"element","typeName":"Col","props":{"label":"Year","data":[1991,1995,1995]},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"tbl"},{"type":"element","typeName":"FollowUpBlock","props":{"items":[]},"partial":true,"hasDynamicProps":false,"statementId":"followUps"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["fu1"],"orphaned":[],"statementCount":7,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":370,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"Top Languages","size":"large-heavy"},"partial":true,"hasDynamicProps":false,"statementId":"title"},{"type":"element","typeName":"Table","props":{"columns":[{"type":"element","typeName":"Col","props":{"label":"Language","data":["Python","JavaScript","Java"]},"partial":true,"hasDynamicProps":false},{"type":"element","typeName":"Col","props":{"label":"Users (M)","data":[15.7,14.2,12.1]},"partial":true,"hasDynamicProps":false},{"type":"element","typeName":"Col","props":{"label":"Year","data":[1991,1995,1995]},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"tbl"},{"type":"element","typeName":"FollowUpBlock","props":{"items":[{"type":"element","typeName":"FollowUpItem","props":{"text":"Tell me m"},"partial":true,"hasDynamicProps":false,"statementId":"fu1"}]},"partial":true,"hasDynamicProps":false,"statementId":"followUps"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["fu2"],"orphaned":[],"statementCount":8,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":407,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"Top Languages","size":"large-heavy"},"partial":false,"hasDynamicProps":false,"statementId":"title"},{"type":"element","typeName":"Table","props":{"columns":[{"type":"element","typeName":"Col","props":{"label":"Language","data":["Python","JavaScript","Java"]},"partial":false,"hasDynamicProps":false},{"type":"element","typeName":"Col","props":{"label":"Users (M)","data":[15.7,14.2,12.1]},"partial":false,"hasDynamicProps":false},{"type":"element","typeName":"Col","props":{"label":"Year","data":[1991,1995,1995]},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"tbl"},{"type":"element","typeName":"FollowUpBlock","props":{"items":[{"type":"element","typeName":"FollowUpItem","props":{"text":"Tell me more about Python"},"partial":false,"hasDynamicProps":false,"statementId":"fu1"}]},"partial":false,"hasDynamicProps":false,"statementId":"followUps"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["FollowUpItem"],"orphaned":[],"statementCount":9,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":442,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"Top Languages","size":"large-heavy"},"partial":false,"hasDynamicProps":false,"statementId":"title"},{"type":"element","typeName":"Table","props":{"columns":[{"type":"element","typeName":"Col","props":{"label":"Language","data":["Python","JavaScript","Java"]},"partial":false,"hasDynamicProps":false},{"type":"element","typeName":"Col","props":{"label":"Users (M)","data":[15.7,14.2,12.1]},"partial":false,"hasDynamicProps":false},{"type":"element","typeName":"Col","props":{"label":"Year","data":[1991,1995,1995]},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"tbl"},{"type":"element","typeName":"FollowUpBlock","props":{"items":[{"type":"element","typeName":"FollowUpItem","props":{"text":"Tell me more about Python"},"partial":false,"hasDynamicProps":false,"statementId":"fu1"},{"type":"element","typeName":"FollowUpItem","props":{"text":"Show me a JavaScript comparison"},"partial":false,"hasDynamicProps":false,"statementId":"fu2"}]},"partial":false,"hasDynamicProps":false,"statementId":"followUps"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":9,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"openui-example-1","schema":"openui","input":"Example 1 — Table (column-oriented):\n\nroot = Stack([title, tbl])\ntitle = TextContent(\"Top Languages\", \"large-heavy\")\ntbl = Table([Col(\"Language\", langs), Col(\"Users (M)\", users), Col(\"Year\", years)])\nlangs = [\"Python\", \"JavaScript\", \"Java\", \"TypeScript\", \"Go\"]\nusers = [15.7, 14.2, 12.1, 8.5, 5.2]\nyears = [1991, 1995, 1995, 2012, 2009]","checkpoints":[{"at":28,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":56,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["titl"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":84,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["TextContent","tbl"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":112,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"Top Languages","size":"large-hea"},"partial":true,"hasDynamicProps":false,"statementId":"title"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["tbl"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":140,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"Top Languages","size":"large-heavy"},"partial":true,"hasDynamicProps":false,"statementId":"title"},{"type":"element","typeName":"Table","props":{"columns":[]},"partial":true,"hasDynamicProps":false,"statementId":"tbl"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":3,"errors":[{"code":"missing-required","component":"Col","path":"/data","message":"missing required field \"/data\" — signature: Col(label*: string, data*, type: \"string\"|\"number\"|\"action\")","statementId":"tbl"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":168,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"Top Languages","size":"large-heavy"},"partial":true,"hasDynamicProps":false,"statementId":"title"},{"type":"element","typeName":"Table","props":{"columns":[]},"partial":true,"hasDynamicProps":false,"statementId":"tbl"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["langs"],"orphaned":[],"statementCount":3,"errors":[{"code":"null-required","component":"Col","path":"/data","message":"required field \"/data\" cannot be null — signature: Col(label*: string, data*, type: \"string\"|\"number\"|\"action\")","statementId":"tbl"},{"code":"missing-required","component":"Col","path":"/data","message":"missing required field \"/data\" — signature: Col(label*: string, data*, type: \"string\"|\"number\"|\"action\")","statementId":"tbl"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":196,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"Top Languages","size":"large-heavy"},"partial":true,"hasDynamicProps":false,"statementId":"title"},{"type":"element","typeName":"Table","props":{"columns":[]},"partial":true,"hasDynamicProps":false,"statementId":"tbl"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["langs","users","years"],"orphaned":[],"statementCount":3,"errors":[{"code":"null-required","component":"Col","path":"/data","message":"required field \"/data\" cannot be null — signature: Col(label*: string, data*, type: \"string\"|\"number\"|\"action\")","statementId":"tbl"},{"code":"null-required","component":"Col","path":"/data","message":"required field \"/data\" cannot be null — signature: Col(label*: string, data*, type: \"string\"|\"number\"|\"action\")","statementId":"tbl"},{"code":"null-required","component":"Col","path":"/data","message":"required field \"/data\" cannot be null — signature: Col(label*: string, data*, type: \"string\"|\"number\"|\"action\")","statementId":"tbl"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":224,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"Top Languages","size":"large-heavy"},"partial":true,"hasDynamicProps":false,"statementId":"title"},{"type":"element","typeName":"Table","props":{"columns":[{"type":"element","typeName":"Col","props":{"label":"Language","data":["Python","Java"]},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"tbl"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["users","years"],"orphaned":[],"statementCount":4,"errors":[{"code":"null-required","component":"Col","path":"/data","message":"required field \"/data\" cannot be null — signature: Col(label*: string, data*, type: \"string\"|\"number\"|\"action\")","statementId":"tbl"},{"code":"null-required","component":"Col","path":"/data","message":"required field \"/data\" cannot be null — signature: Col(label*: string, data*, type: \"string\"|\"number\"|\"action\")","statementId":"tbl"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":252,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"Top Languages","size":"large-heavy"},"partial":true,"hasDynamicProps":false,"statementId":"title"},{"type":"element","typeName":"Table","props":{"columns":[{"type":"element","typeName":"Col","props":{"label":"Language","data":["Python","JavaScript","Java","TypeScript"]},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"tbl"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["users","years"],"orphaned":[],"statementCount":4,"errors":[{"code":"null-required","component":"Col","path":"/data","message":"required field \"/data\" cannot be null — signature: Col(label*: string, data*, type: \"string\"|\"number\"|\"action\")","statementId":"tbl"},{"code":"null-required","component":"Col","path":"/data","message":"required field \"/data\" cannot be null — signature: Col(label*: string, data*, type: \"string\"|\"number\"|\"action\")","statementId":"tbl"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":280,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"Top Languages","size":"large-heavy"},"partial":true,"hasDynamicProps":false,"statementId":"title"},{"type":"element","typeName":"Table","props":{"columns":[{"type":"element","typeName":"Col","props":{"label":"Language","data":["Python","JavaScript","Java","TypeScript","Go"]},"partial":true,"hasDynamicProps":false},{"type":"element","typeName":"Col","props":{"label":"Users (M)","data":[15.7,14.2]},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"tbl"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["years"],"orphaned":[],"statementCount":5,"errors":[{"code":"null-required","component":"Col","path":"/data","message":"required field \"/data\" cannot be null — signature: Col(label*: string, data*, type: \"string\"|\"number\"|\"action\")","statementId":"tbl"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":308,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"Top Languages","size":"large-heavy"},"partial":true,"hasDynamicProps":false,"statementId":"title"},{"type":"element","typeName":"Table","props":{"columns":[{"type":"element","typeName":"Col","props":{"label":"Language","data":["Python","JavaScript","Java","TypeScript","Go"]},"partial":true,"hasDynamicProps":false},{"type":"element","typeName":"Col","props":{"label":"Users (M)","data":[15.7,14.2,12.1,8.5,5.2]},"partial":true,"hasDynamicProps":false},{"type":"element","typeName":"Col","props":{"label":"Year","data":[1]},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"tbl"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":6,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":336,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"Top Languages","size":"large-heavy"},"partial":false,"hasDynamicProps":false,"statementId":"title"},{"type":"element","typeName":"Table","props":{"columns":[{"type":"element","typeName":"Col","props":{"label":"Language","data":["Python","JavaScript","Java","TypeScript","Go"]},"partial":false,"hasDynamicProps":false},{"type":"element","typeName":"Col","props":{"label":"Users (M)","data":[15.7,14.2,12.1,8.5,5.2]},"partial":false,"hasDynamicProps":false},{"type":"element","typeName":"Col","props":{"label":"Year","data":[1991,1995,1995,2012,2009]},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"tbl"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":6,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"openui-example-2","schema":"openui","input":"Example 2 — Bar chart:\n\nroot = Stack([title, chart])\ntitle = TextContent(\"Q4 Revenue\", \"large-heavy\")\nchart = BarChart(labels, [s1, s2], \"grouped\")\nlabels = [\"Oct\", \"Nov\", \"Dec\"]\ns1 = Series(\"Product A\", [120, 150, 180])\ns2 = Series(\"Product B\", [90, 110, 140])","checkpoints":[{"at":22,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":44,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["title"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":66,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["TextC","chart"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":88,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"Q4 Revenue","size":""},"partial":true,"hasDynamicProps":false,"statementId":"title"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["chart"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":110,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"Q4 Revenue","size":"large-heavy"},"partial":false,"hasDynamicProps":false,"statementId":"title"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["chart"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":132,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"Q4 Revenue","size":"large-heavy"},"partial":true,"hasDynamicProps":false,"statementId":"title"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["labels","s1"],"orphaned":[],"statementCount":3,"errors":[{"code":"null-required","component":"BarChart","path":"/labels","message":"required field \"/labels\" cannot be null — signature: BarChart(labels*: array, series*: array, variant: \"grouped\"|\"stacked\", xLabel: string, yLabel: string, height: number)","statementId":"chart"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":154,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"Q4 Revenue","size":"large-heavy"},"partial":false,"hasDynamicProps":false,"statementId":"title"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["labels","s1","s2"],"orphaned":[],"statementCount":3,"errors":[{"code":"null-required","component":"BarChart","path":"/labels","message":"required field \"/labels\" cannot be null — signature: BarChart(labels*: array, series*: array, variant: \"grouped\"|\"stacked\", xLabel: string, yLabel: string, height: number)","statementId":"chart"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":176,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"Q4 Revenue","size":"large-heavy"},"partial":true,"hasDynamicProps":false,"statementId":"title"},{"type":"element","typeName":"BarChart","props":{"labels":["Oct","Nov","Dec"],"series":[],"variant":"grouped"},"partial":true,"hasDynamicProps":false,"statementId":"chart"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["s1","s2"],"orphaned":[],"statementCount":4,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":198,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"Q4 Revenue","size":"large-heavy"},"partial":true,"hasDynamicProps":false,"statementId":"title"},{"type":"element","typeName":"BarChart","props":{"labels":["Oct","Nov","Dec"],"series":[],"variant":"grouped"},"partial":true,"hasDynamicProps":false,"statementId":"chart"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["s2"],"orphaned":[],"statementCount":5,"errors":[{"code":"missing-required","component":"Series","path":"/values","message":"missing required field \"/values\" — signature: Series(category*: string, values*: array)","statementId":"s1"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":220,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"Q4 Revenue","size":"large-heavy"},"partial":false,"hasDynamicProps":false,"statementId":"title"},{"type":"element","typeName":"BarChart","props":{"labels":["Oct","Nov","Dec"],"series":[{"type":"element","typeName":"Series","props":{"category":"Product A","values":[120,150,180]},"partial":false,"hasDynamicProps":false,"statementId":"s1"}],"variant":"grouped"},"partial":false,"hasDynamicProps":false,"statementId":"chart"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["s2"],"orphaned":[],"statementCount":5,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":242,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"Q4 Revenue","size":"large-heavy"},"partial":true,"hasDynamicProps":false,"statementId":"title"},{"type":"element","typeName":"BarChart","props":{"labels":["Oct","Nov","Dec"],"series":[{"type":"element","typeName":"Series","props":{"category":"Product A","values":[120,150,180]},"partial":true,"hasDynamicProps":false,"statementId":"s1"}],"variant":"grouped"},"partial":true,"hasDynamicProps":false,"statementId":"chart"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":6,"errors":[{"code":"missing-required","component":"Series","path":"/values","message":"missing required field \"/values\" — signature: Series(category*: string, values*: array)","statementId":"s2"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":261,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"Q4 Revenue","size":"large-heavy"},"partial":false,"hasDynamicProps":false,"statementId":"title"},{"type":"element","typeName":"BarChart","props":{"labels":["Oct","Nov","Dec"],"series":[{"type":"element","typeName":"Series","props":{"category":"Product A","values":[120,150,180]},"partial":false,"hasDynamicProps":false,"statementId":"s1"},{"type":"element","typeName":"Series","props":{"category":"Product B","values":[90,110,140]},"partial":false,"hasDynamicProps":false,"statementId":"s2"}],"variant":"grouped"},"partial":false,"hasDynamicProps":false,"statementId":"chart"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":6,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"openui-example-3","schema":"openui","input":"Example 3 — Form with validation:\n\nroot = Stack([title, form])\ntitle = TextContent(\"Contact Us\", \"large-heavy\")\nform = Form(\"contact\", btns, [nameField, emailField, countryField, msgField])\nnameField = FormControl(\"Name\", Input(\"name\", \"Your name\", \"text\", { required: true, minLength: 2 }))\nemailField = FormControl(\"Email\", Input(\"email\", \"you@example.com\", \"email\", { required: true, email: true }))\ncountryField = FormControl(\"Country\", Select(\"country\", countryOpts, \"Select...\", { required: true }))\nmsgField = FormControl(\"Message\", TextArea(\"message\", \"Tell us more...\", 4, { required: true, minLength: 10 }))\ncountryOpts = [SelectItem(\"us\", \"United States\"), SelectItem(\"uk\", \"United Kingdom\"), SelectItem(\"de\", \"Germany\")]\nbtns = Buttons([Button(\"Submit\", Action([@ToAssistant(\"Submit\")]), \"primary\"), Button(\"Cancel\", Action([@ToAssistant(\"Cancel\")]), \"secondary\")])","checkpoints":[{"at":74,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["Tex","form"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":148,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"Contact Us","size":"large-heavy"},"partial":true,"hasDynamicProps":false,"statementId":"title"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["btns","nameFi"],"orphaned":[],"statementCount":3,"errors":[{"code":"null-required","component":"Form","path":"/buttons","message":"required field \"/buttons\" cannot be null — signature: Form(name*: string, buttons*: Buttons, fields*: array)","statementId":"form"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":222,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"Contact Us","size":"large-heavy"},"partial":true,"hasDynamicProps":false,"statementId":"title"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["btns","emailField","countryField","msgField"],"orphaned":[],"statementCount":4,"errors":[{"code":"missing-required","component":"FormControl","path":"/input","message":"missing required field \"/input\" — signature: FormControl(label*: string, input*, hint: string)","statementId":"nameField"},{"code":"null-required","component":"Form","path":"/buttons","message":"required field \"/buttons\" cannot be null — signature: Form(name*: string, buttons*: Buttons, fields*: array)","statementId":"form"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":296,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"Contact Us","size":"large-heavy"},"partial":false,"hasDynamicProps":false,"statementId":"title"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["btns","emailField","countryField","msgField"],"orphaned":[],"statementCount":4,"errors":[{"code":"null-required","component":"Form","path":"/buttons","message":"required field \"/buttons\" cannot be null — signature: Form(name*: string, buttons*: Buttons, fields*: array)","statementId":"form"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":370,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"Contact Us","size":"large-heavy"},"partial":true,"hasDynamicProps":false,"statementId":"title"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["btns","countryField","msgField"],"orphaned":[],"statementCount":5,"errors":[{"code":"null-required","component":"Form","path":"/buttons","message":"required field \"/buttons\" cannot be null — signature: Form(name*: string, buttons*: Buttons, fields*: array)","statementId":"form"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":444,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"Contact Us","size":"large-heavy"},"partial":true,"hasDynamicProps":false,"statementId":"title"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["btns","Sel","msgField"],"orphaned":[],"statementCount":6,"errors":[{"code":"null-required","component":"FormControl","path":"/input","message":"required field \"/input\" cannot be null — signature: FormControl(label*: string, input*, hint: string)","statementId":"countryField"},{"code":"null-required","component":"Form","path":"/buttons","message":"required field \"/buttons\" cannot be null — signature: Form(name*: string, buttons*: Buttons, fields*: array)","statementId":"form"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":518,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"Contact Us","size":"large-heavy"},"partial":false,"hasDynamicProps":false,"statementId":"title"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["btns","countryOpts","F"],"orphaned":[],"statementCount":7,"errors":[{"code":"null-required","component":"Select","path":"/items","message":"required field \"/items\" cannot be null — signature: Select(name*: string, items*: array, placeholder: string, rules: object, value: string, size: \"small\"|\"medium\"|\"large\")","statementId":"countryField"},{"code":"null-required","component":"FormControl","path":"/input","message":"required field \"/input\" cannot be null — signature: FormControl(label*: string, input*, hint: string)","statementId":"countryField"},{"code":"null-required","component":"Form","path":"/buttons","message":"required field \"/buttons\" cannot be null — signature: Form(name*: string, buttons*: Buttons, fields*: array)","statementId":"form"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":592,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"Contact Us","size":"large-heavy"},"partial":true,"hasDynamicProps":false,"statementId":"title"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["btns","countryOpts"],"orphaned":[],"statementCount":7,"errors":[{"code":"null-required","component":"Select","path":"/items","message":"required field \"/items\" cannot be null — signature: Select(name*: string, items*: array, placeholder: string, rules: object, value: string, size: \"small\"|\"medium\"|\"large\")","statementId":"countryField"},{"code":"null-required","component":"FormControl","path":"/input","message":"required field \"/input\" cannot be null — signature: FormControl(label*: string, input*, hint: string)","statementId":"countryField"},{"code":"null-required","component":"Form","path":"/buttons","message":"required field \"/buttons\" cannot be null — signature: Form(name*: string, buttons*: Buttons, fields*: array)","statementId":"form"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":666,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"Contact Us","size":"large-heavy"},"partial":true,"hasDynamicProps":false,"statementId":"title"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["btns"],"orphaned":[],"statementCount":8,"errors":[{"code":"null-required","component":"Form","path":"/buttons","message":"required field \"/buttons\" cannot be null — signature: Form(name*: string, buttons*: Buttons, fields*: array)","statementId":"form"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":740,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"Contact Us","size":"large-heavy"},"partial":false,"hasDynamicProps":false,"statementId":"title"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["btns"],"orphaned":[],"statementCount":8,"errors":[{"code":"null-required","component":"Form","path":"/buttons","message":"required field \"/buttons\" cannot be null — signature: Form(name*: string, buttons*: Buttons, fields*: array)","statementId":"form"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":814,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"Contact Us","size":"large-heavy"},"partial":true,"hasDynamicProps":false,"statementId":"title"},{"type":"element","typeName":"Form","props":{"name":"contact","buttons":{"type":"element","typeName":"Buttons","props":{"buttons":[{"type":"element","typeName":"Button","props":{"label":"Submit","action":{"k":"Comp","name":"Action","args":[{"k":"Arr","els":[{"k":"Comp","name":"ToAssistant","args":[{"k":"Str","v":"Submit"}]}]}]},"variant":"primary"},"partial":true,"hasDynamicProps":true}]},"partial":true,"hasDynamicProps":true,"statementId":"btns"},"fields":[{"type":"element","typeName":"FormControl","props":{"label":"Name","input":{"type":"element","typeName":"Input","props":{"name":"name","placeholder":"Your name","type":"text","rules":{"required":true,"minLength":2}},"partial":true,"hasDynamicProps":false}},"partial":true,"hasDynamicProps":false,"statementId":"nameField"},{"type":"element","typeName":"FormControl","props":{"label":"Email","input":{"type":"element","typeName":"Input","props":{"name":"email","placeholder":"you@example.com","type":"email","rules":{"required":true,"email":true}},"partial":true,"hasDynamicProps":false}},"partial":true,"hasDynamicProps":false,"statementId":"emailField"},{"type":"element","typeName":"FormControl","props":{"label":"Country","input":{"type":"element","typeName":"Select","props":{"name":"country","items":[{"type":"element","typeName":"SelectItem","props":{"value":"us","label":"United States"},"partial":true,"hasDynamicProps":false},{"type":"element","typeName":"SelectItem","props":{"value":"uk","label":"United Kingdom"},"partial":true,"hasDynamicProps":false},{"type":"element","typeName":"SelectItem","props":{"value":"de","label":"Germany"},"partial":true,"hasDynamicProps":false}],"placeholder":"Select...","rules":{"required":true}},"partial":true,"hasDynamicProps":false}},"partial":true,"hasDynamicProps":false,"statementId":"countryField"},{"type":"element","typeName":"FormControl","props":{"label":"Message","input":{"type":"element","typeName":"TextArea","props":{"name":"message","placeholder":"Tell us more...","rows":4,"rules":{"required":true,"minLength":10}},"partial":true,"hasDynamicProps":false}},"partial":true,"hasDynamicProps":false,"statementId":"msgField"}]},"partial":true,"hasDynamicProps":true,"statementId":"form"}]},"partial":true,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["Bu"],"orphaned":[],"statementCount":9,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":877,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"Contact Us","size":"large-heavy"},"partial":false,"hasDynamicProps":false,"statementId":"title"},{"type":"element","typeName":"Form","props":{"name":"contact","buttons":{"type":"element","typeName":"Buttons","props":{"buttons":[{"type":"element","typeName":"Button","props":{"label":"Submit","action":{"k":"Comp","name":"Action","args":[{"k":"Arr","els":[{"k":"Comp","name":"ToAssistant","args":[{"k":"Str","v":"Submit"}]}]}]},"variant":"primary"},"partial":false,"hasDynamicProps":true},{"type":"element","typeName":"Button","props":{"label":"Cancel","action":{"k":"Comp","name":"Action","args":[{"k":"Arr","els":[{"k":"Comp","name":"ToAssistant","args":[{"k":"Str","v":"Cancel"}]}]}]},"variant":"secondary"},"partial":false,"hasDynamicProps":true}]},"partial":false,"hasDynamicProps":true,"statementId":"btns"},"fields":[{"type":"element","typeName":"FormControl","props":{"label":"Name","input":{"type":"element","typeName":"Input","props":{"name":"name","placeholder":"Your name","type":"text","rules":{"required":true,"minLength":2}},"partial":false,"hasDynamicProps":false}},"partial":false,"hasDynamicProps":false,"statementId":"nameField"},{"type":"element","typeName":"FormControl","props":{"label":"Email","input":{"type":"element","typeName":"Input","props":{"name":"email","placeholder":"you@example.com","type":"email","rules":{"required":true,"email":true}},"partial":false,"hasDynamicProps":false}},"partial":false,"hasDynamicProps":false,"statementId":"emailField"},{"type":"element","typeName":"FormControl","props":{"label":"Country","input":{"type":"element","typeName":"Select","props":{"name":"country","items":[{"type":"element","typeName":"SelectItem","props":{"value":"us","label":"United States"},"partial":false,"hasDynamicProps":false},{"type":"element","typeName":"SelectItem","props":{"value":"uk","label":"United Kingdom"},"partial":false,"hasDynamicProps":false},{"type":"element","typeName":"SelectItem","props":{"value":"de","label":"Germany"},"partial":false,"hasDynamicProps":false}],"placeholder":"Select...","rules":{"required":true}},"partial":false,"hasDynamicProps":false}},"partial":false,"hasDynamicProps":false,"statementId":"countryField"},{"type":"element","typeName":"FormControl","props":{"label":"Message","input":{"type":"element","typeName":"TextArea","props":{"name":"message","placeholder":"Tell us more...","rows":4,"rules":{"required":true,"minLength":10}},"partial":false,"hasDynamicProps":false}},"partial":false,"hasDynamicProps":false,"statementId":"msgField"}]},"partial":false,"hasDynamicProps":true,"statementId":"form"}]},"partial":false,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":9,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"openui-example-4","schema":"openui","input":"Example 4 — Tabs with mixed content:\n\nroot = Stack([title, tabs])\ntitle = TextContent(\"React vs Vue\", \"large-heavy\")\ntabs = Tabs([tabReact, tabVue])\ntabReact = TabItem(\"react\", \"React\", reactContent)\ntabVue = TabItem(\"vue\", \"Vue\", vueContent)\nreactContent = [TextContent(\"React is a library by Meta for building UIs.\"), Callout(\"info\", \"Note\", \"React uses JSX syntax.\")]\nvueContent = [TextContent(\"Vue is a progressive framework by Evan You.\"), Callout(\"success\", \"Tip\", \"Vue has a gentle learning curve.\")]","checkpoints":[{"at":43,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":86,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["tabs"],"orphaned":[],"statementCount":2,"errors":[{"code":"missing-required","component":"TextContent","path":"/text","message":"missing required field \"/text\" — signature: TextContent(text*: string, size: \"small\"|\"default\"|\"large\"|\"small-heavy\"|\"large-heavy\")","statementId":"title"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":129,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"React vs Vue","size":"large-heavy"},"partial":true,"hasDynamicProps":false,"statementId":"title"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":3,"errors":[{"code":"missing-required","component":"Tabs","path":"/items","message":"missing required field \"/items\" — signature: Tabs(items*: array)","statementId":"tabs"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":172,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"React vs Vue","size":"large-heavy"},"partial":true,"hasDynamicProps":false,"statementId":"title"},{"type":"element","typeName":"Tabs","props":{"items":[]},"partial":true,"hasDynamicProps":false,"statementId":"tabs"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["tabVue"],"orphaned":[],"statementCount":4,"errors":[{"code":"missing-required","component":"TabItem","path":"/trigger","message":"missing required field \"/trigger\" — signature: TabItem(value*: string, trigger*: string, content*: array)","statementId":"tabReact"},{"code":"missing-required","component":"TabItem","path":"/content","message":"missing required field \"/content\" — signature: TabItem(value*: string, trigger*: string, content*: array)","statementId":"tabReact"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":215,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"React vs Vue","size":"large-heavy"},"partial":false,"hasDynamicProps":false,"statementId":"title"},{"type":"element","typeName":"Tabs","props":{"items":[]},"partial":false,"hasDynamicProps":false,"statementId":"tabs"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["reactContent","TabIte"],"orphaned":[],"statementCount":5,"errors":[{"code":"null-required","component":"TabItem","path":"/content","message":"required field \"/content\" cannot be null — signature: TabItem(value*: string, trigger*: string, content*: array)","statementId":"tabReact"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":258,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"React vs Vue","size":"large-heavy"},"partial":false,"hasDynamicProps":false,"statementId":"title"},{"type":"element","typeName":"Tabs","props":{"items":[]},"partial":false,"hasDynamicProps":false,"statementId":"tabs"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["reactContent","vueContent"],"orphaned":[],"statementCount":5,"errors":[{"code":"null-required","component":"TabItem","path":"/content","message":"required field \"/content\" cannot be null — signature: TabItem(value*: string, trigger*: string, content*: array)","statementId":"tabReact"},{"code":"null-required","component":"TabItem","path":"/content","message":"required field \"/content\" cannot be null — signature: TabItem(value*: string, trigger*: string, content*: array)","statementId":"tabVue"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":301,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"React vs Vue","size":"large-heavy"},"partial":true,"hasDynamicProps":false,"statementId":"title"},{"type":"element","typeName":"Tabs","props":{"items":[{"type":"element","typeName":"TabItem","props":{"value":"react","trigger":"React","content":[{"type":"element","typeName":"TextContent","props":{"text":"React is a library by Meta fo"},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"tabReact"}]},"partial":true,"hasDynamicProps":false,"statementId":"tabs"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["vueContent"],"orphaned":[],"statementCount":6,"errors":[{"code":"null-required","component":"TabItem","path":"/content","message":"required field \"/content\" cannot be null — signature: TabItem(value*: string, trigger*: string, content*: array)","statementId":"tabVue"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":344,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"React vs Vue","size":"large-heavy"},"partial":true,"hasDynamicProps":false,"statementId":"title"},{"type":"element","typeName":"Tabs","props":{"items":[{"type":"element","typeName":"TabItem","props":{"value":"react","trigger":"React","content":[{"type":"element","typeName":"TextContent","props":{"text":"React is a library by Meta for building UIs."},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"tabReact"}]},"partial":true,"hasDynamicProps":false,"statementId":"tabs"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["vueContent"],"orphaned":[],"statementCount":6,"errors":[{"code":"missing-required","component":"Callout","path":"/description","message":"missing required field \"/description\" — signature: Callout(variant*: \"info\"|\"warning\"|\"error\"|\"success\"|\"neutral\", title*: string, description*: string, visible: boolean)","statementId":"reactContent"},{"code":"null-required","component":"TabItem","path":"/content","message":"required field \"/content\" cannot be null — signature: TabItem(value*: string, trigger*: string, content*: array)","statementId":"tabVue"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":387,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"React vs Vue","size":"large-heavy"},"partial":true,"hasDynamicProps":false,"statementId":"title"},{"type":"element","typeName":"Tabs","props":{"items":[{"type":"element","typeName":"TabItem","props":{"value":"react","trigger":"React","content":[{"type":"element","typeName":"TextContent","props":{"text":"React is a library by Meta for building UIs."},"partial":true,"hasDynamicProps":false},{"type":"element","typeName":"Callout","props":{"variant":"info","title":"Note","description":"React uses JSX syntax."},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"tabReact"},{"type":"element","typeName":"TabItem","props":{"value":"vue","trigger":"Vue","content":[]},"partial":true,"hasDynamicProps":false,"statementId":"tabVue"}]},"partial":true,"hasDynamicProps":false,"statementId":"tabs"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["Te"],"orphaned":[],"statementCount":7,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":430,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"React vs Vue","size":"large-heavy"},"partial":true,"hasDynamicProps":false,"statementId":"title"},{"type":"element","typeName":"Tabs","props":{"items":[{"type":"element","typeName":"TabItem","props":{"value":"react","trigger":"React","content":[{"type":"element","typeName":"TextContent","props":{"text":"React is a library by Meta for building UIs."},"partial":true,"hasDynamicProps":false},{"type":"element","typeName":"Callout","props":{"variant":"info","title":"Note","description":"React uses JSX syntax."},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"tabReact"},{"type":"element","typeName":"TabItem","props":{"value":"vue","trigger":"Vue","content":[{"type":"element","typeName":"TextContent","props":{"text":"Vue is a progressive framework b"},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"tabVue"}]},"partial":true,"hasDynamicProps":false,"statementId":"tabs"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":7,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":473,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"React vs Vue","size":"large-heavy"},"partial":true,"hasDynamicProps":false,"statementId":"title"},{"type":"element","typeName":"Tabs","props":{"items":[{"type":"element","typeName":"TabItem","props":{"value":"react","trigger":"React","content":[{"type":"element","typeName":"TextContent","props":{"text":"React is a library by Meta for building UIs."},"partial":true,"hasDynamicProps":false},{"type":"element","typeName":"Callout","props":{"variant":"info","title":"Note","description":"React uses JSX syntax."},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"tabReact"},{"type":"element","typeName":"TabItem","props":{"value":"vue","trigger":"Vue","content":[{"type":"element","typeName":"TextContent","props":{"text":"Vue is a progressive framework by Evan You."},"partial":true,"hasDynamicProps":false},{"type":"element","typeName":"Callout","props":{"variant":"success","title":"Tip","description":"V"},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"tabVue"}]},"partial":true,"hasDynamicProps":false,"statementId":"tabs"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":7,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":507,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"TextContent","props":{"text":"React vs Vue","size":"large-heavy"},"partial":false,"hasDynamicProps":false,"statementId":"title"},{"type":"element","typeName":"Tabs","props":{"items":[{"type":"element","typeName":"TabItem","props":{"value":"react","trigger":"React","content":[{"type":"element","typeName":"TextContent","props":{"text":"React is a library by Meta for building UIs."},"partial":false,"hasDynamicProps":false},{"type":"element","typeName":"Callout","props":{"variant":"info","title":"Note","description":"React uses JSX syntax."},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"tabReact"},{"type":"element","typeName":"TabItem","props":{"value":"vue","trigger":"Vue","content":[{"type":"element","typeName":"TextContent","props":{"text":"Vue is a progressive framework by Evan You."},"partial":false,"hasDynamicProps":false},{"type":"element","typeName":"Callout","props":{"variant":"success","title":"Tip","description":"Vue has a gentle learning curve."},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"tabVue"}]},"partial":false,"hasDynamicProps":false,"statementId":"tabs"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":7,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"openui-example-5","schema":"openui","input":"Example 5 — KPI and product cards:\n\nroot = Stack([kpiHeader, kpiBlock, productsHeader, productCards])\nkpiHeader = InlineHeader(\"This Month\", \"Key account metrics\")\nkpiBlock = SnippetCardBlock([kpi1, kpi2, kpi3])\nkpi1 = SnippetCardItem(\"revenue\", kpi1lhs, kpi1rhs)\nkpi1lhs = IconText(kpi1icon, \"neutral\", \"m\", \"Revenue\", \"Month to date\", false, \"horizontal\")\nkpi1icon = Icon(\"circle-dollar-sign\", \"finance\")\nkpi1rhs = BoldText(\"number\", \"$48,200\", \"+8.1%\", \"metric\")\nkpi2 = SnippetCardItem(\"active-users\", kpi2lhs, kpi2rhs)\nkpi2lhs = IconText(kpi2icon, \"neutral\", \"m\", \"Active Users\", \"This month\", false, \"horizontal\")\nkpi2icon = Icon(\"users\", \"people\")\nkpi2rhs = BoldText(\"number\", \"1,204\", \"+3.4%\", \"metric\")\nkpi3 = SnippetCardItem(\"churn\", kpi3lhs, kpi3rhs)\nkpi3lhs = IconText(kpi3icon, \"neutral\", \"m\", \"Churn\", \"This month\", false, \"horizontal\")\nkpi3icon = Icon(\"user-minus\", \"people\")\nkpi3rhs = BoldText(\"number\", \"1.8%\", \"-0.3%\", \"metric\")\nproductsHeader = InlineHeader(\"Top Products\", \"By units sold\")\nproductCards = CompositeCardBlock([p1, p2])\np1 = CompositeCardItem(\"widget-pro\", p1header, [p1body], p1footer)\np1header = IconText(p1icon, \"neutral\", \"m\", \"Widget Pro\", \"Best seller\", true, \"horizontal\")\np1icon = Icon(\"package\", \"shopping\")\np1body = MetricIndicatorInline(\"2,410 units\", \"Sold this month\", { direction: \"up\", value: 12 })\np1footer = { price: BoldText(\"text\", \"$29.00\"), button: p1btn }\np1btn = Button(\"View Details\", Action([@ToAssistant(\"Show details for Widget Pro\")]), \"secondary\")\np2 = CompositeCardItem(\"widget-mini\", p2header, [p2body], p2footer)\np2header = IconText(p2icon, \"neutral\", \"m\", \"Widget Mini\", \"New arrival\", true, \"horizontal\")\np2icon = Icon(\"box\", \"shopping\")\np2body = MetricIndicatorInline(\"980 units\", \"Sold this month\", { direction: \"up\", value: 4 })\np2footer = { price: BoldText(\"text\", \"$14.00\"), button: p2btn }\np2btn = Button(\"View Details\", Action([@ToAssistant(\"Show details for Widget Mini\")]), \"secondary\")","checkpoints":[{"at":164,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"InlineHeader","props":{"heading":"This Month","description":"Key account metrics"},"partial":false,"hasDynamicProps":false,"statementId":"kpiHeader"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["kpiBlock","productsHeader","productCards"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":328,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"InlineHeader","props":{"heading":"This Month","description":"Key account metrics"},"partial":true,"hasDynamicProps":false,"statementId":"kpiHeader"},{"type":"element","typeName":"SnippetCardBlock","props":{"items":[],"layout":"grid","responsive":true},"partial":true,"hasDynamicProps":false,"statementId":"kpiBlock"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["kpi1icon","kpi1rhs","kpi2","kpi3","productsHeader","productCards"],"orphaned":[],"statementCount":5,"errors":[{"code":"null-required","component":"IconText","path":"/icon","message":"required field \"/icon\" cannot be null — signature: IconText(icon*: Icon, iconVariant*: \"neutral\"|\"info\"|\"success\"|\"warning\"|\"danger\"|\"inverted\"|\"filled\"|\"soft\", iconSize*: \"xs\"|\"s\"|\"m\"|\"l\"|\"xl\"|\"sm\"|\"md\"|\"lg\", title*: string, subtitle: string, bold*: boolean, layout*: \"horizontal\"|\"vertical\")","statementId":"kpi1lhs"},{"code":"null-required","component":"SnippetCardItem","path":"/lhs","message":"required field \"/lhs\" cannot be null — signature: SnippetCardItem(id: string, lhs*, rhs)","statementId":"kpi1"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":492,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"InlineHeader","props":{"heading":"This Month","description":"Key account metrics"},"partial":true,"hasDynamicProps":false,"statementId":"kpiHeader"},{"type":"element","typeName":"SnippetCardBlock","props":{"items":[{"type":"element","typeName":"SnippetCardItem","props":{"id":"revenue","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"circle-dollar-sign","category":"finance"},"partial":true,"hasDynamicProps":false,"statementId":"kpi1icon"},"iconVariant":"neutral","iconSize":"m","title":"Revenue","subtitle":"Month to date","bold":false,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"kpi1lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"$48,200","subtext":"+8.1%","subtextVariant":"metric","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"kpi1rhs"}},"partial":true,"hasDynamicProps":false,"statementId":"kpi1"}],"layout":"grid","responsive":true},"partial":true,"hasDynamicProps":false,"statementId":"kpiBlock"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["kpi3","productsHeader","productCards"],"orphaned":[],"statementCount":8,"errors":[{"code":"missing-required","component":"SnippetCardItem","path":"/lhs","message":"missing required field \"/lhs\" — signature: SnippetCardItem(id: string, lhs*, rhs)","statementId":"kpi2"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":656,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"InlineHeader","props":{"heading":"This Month","description":"Key account metrics"},"partial":false,"hasDynamicProps":false,"statementId":"kpiHeader"},{"type":"element","typeName":"SnippetCardBlock","props":{"items":[{"type":"element","typeName":"SnippetCardItem","props":{"id":"revenue","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"circle-dollar-sign","category":"finance"},"partial":false,"hasDynamicProps":false,"statementId":"kpi1icon"},"iconVariant":"neutral","iconSize":"m","title":"Revenue","subtitle":"Month to date","bold":false,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"kpi1lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"$48,200","subtext":"+8.1%","subtextVariant":"metric","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"kpi1rhs"}},"partial":false,"hasDynamicProps":false,"statementId":"kpi1"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"active-users","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"users","category":"people"},"partial":false,"hasDynamicProps":false,"statementId":"kpi2icon"},"iconVariant":"neutral","iconSize":"m","title":"Active Users","subtitle":"This month","bold":false,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"kpi2lhs"},"rhs":null},"partial":false,"hasDynamicProps":false,"statementId":"kpi2"}],"layout":"grid","responsive":true},"partial":false,"hasDynamicProps":false,"statementId":"kpiBlock"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["kpi2rhs","kpi3","productsHeader","productCards"],"orphaned":[],"statementCount":10,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":820,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"InlineHeader","props":{"heading":"This Month","description":"Key account metrics"},"partial":true,"hasDynamicProps":false,"statementId":"kpiHeader"},{"type":"element","typeName":"SnippetCardBlock","props":{"items":[{"type":"element","typeName":"SnippetCardItem","props":{"id":"revenue","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"circle-dollar-sign","category":"finance"},"partial":true,"hasDynamicProps":false,"statementId":"kpi1icon"},"iconVariant":"neutral","iconSize":"m","title":"Revenue","subtitle":"Month to date","bold":false,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"kpi1lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"$48,200","subtext":"+8.1%","subtextVariant":"metric","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"kpi1rhs"}},"partial":true,"hasDynamicProps":false,"statementId":"kpi1"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"active-users","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"users","category":"people"},"partial":true,"hasDynamicProps":false,"statementId":"kpi2icon"},"iconVariant":"neutral","iconSize":"m","title":"Active Users","subtitle":"This month","bold":false,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"kpi2lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"1,204","subtext":"+3.4%","subtextVariant":"metric","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"kpi2rhs"}},"partial":true,"hasDynamicProps":false,"statementId":"kpi2"}],"layout":"grid","responsive":true},"partial":true,"hasDynamicProps":false,"statementId":"kpiBlock"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["kpi3icon","kpi3rhs","productsHeader","productCards"],"orphaned":[],"statementCount":13,"errors":[{"code":"null-required","component":"IconText","path":"/icon","message":"required field \"/icon\" cannot be null — signature: IconText(icon*: Icon, iconVariant*: \"neutral\"|\"info\"|\"success\"|\"warning\"|\"danger\"|\"inverted\"|\"filled\"|\"soft\", iconSize*: \"xs\"|\"s\"|\"m\"|\"l\"|\"xl\"|\"sm\"|\"md\"|\"lg\", title*: string, subtitle: string, bold*: boolean, layout*: \"horizontal\"|\"vertical\")","statementId":"kpi3lhs"},{"code":"null-required","component":"SnippetCardItem","path":"/lhs","message":"required field \"/lhs\" cannot be null — signature: SnippetCardItem(id: string, lhs*, rhs)","statementId":"kpi3"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":984,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"InlineHeader","props":{"heading":"This Month","description":"Key account metrics"},"partial":true,"hasDynamicProps":false,"statementId":"kpiHeader"},{"type":"element","typeName":"SnippetCardBlock","props":{"items":[{"type":"element","typeName":"SnippetCardItem","props":{"id":"revenue","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"circle-dollar-sign","category":"finance"},"partial":true,"hasDynamicProps":false,"statementId":"kpi1icon"},"iconVariant":"neutral","iconSize":"m","title":"Revenue","subtitle":"Month to date","bold":false,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"kpi1lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"$48,200","subtext":"+8.1%","subtextVariant":"metric","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"kpi1rhs"}},"partial":true,"hasDynamicProps":false,"statementId":"kpi1"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"active-users","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"users","category":"people"},"partial":true,"hasDynamicProps":false,"statementId":"kpi2icon"},"iconVariant":"neutral","iconSize":"m","title":"Active Users","subtitle":"This month","bold":false,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"kpi2lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"1,204","subtext":"+3.4%","subtextVariant":"metric","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"kpi2rhs"}},"partial":true,"hasDynamicProps":false,"statementId":"kpi2"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"churn","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"user-minus","category":"people"},"partial":true,"hasDynamicProps":false,"statementId":"kpi3icon"},"iconVariant":"neutral","iconSize":"m","title":"Churn","subtitle":"This month","bold":false,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"kpi3lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"1.8%","subtext":"-0.3%","subtextVariant":"metric","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"kpi3rhs"}},"partial":true,"hasDynamicProps":false,"statementId":"kpi3"}],"layout":"grid","responsive":true},"partial":true,"hasDynamicProps":false,"statementId":"kpiBlock"},{"type":"element","typeName":"InlineHeader","props":{"heading":"Top Pro"},"partial":true,"hasDynamicProps":false,"statementId":"productsHeader"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["productCards"],"orphaned":[],"statementCount":16,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":1148,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"InlineHeader","props":{"heading":"This Month","description":"Key account metrics"},"partial":true,"hasDynamicProps":false,"statementId":"kpiHeader"},{"type":"element","typeName":"SnippetCardBlock","props":{"items":[{"type":"element","typeName":"SnippetCardItem","props":{"id":"revenue","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"circle-dollar-sign","category":"finance"},"partial":true,"hasDynamicProps":false,"statementId":"kpi1icon"},"iconVariant":"neutral","iconSize":"m","title":"Revenue","subtitle":"Month to date","bold":false,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"kpi1lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"$48,200","subtext":"+8.1%","subtextVariant":"metric","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"kpi1rhs"}},"partial":true,"hasDynamicProps":false,"statementId":"kpi1"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"active-users","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"users","category":"people"},"partial":true,"hasDynamicProps":false,"statementId":"kpi2icon"},"iconVariant":"neutral","iconSize":"m","title":"Active Users","subtitle":"This month","bold":false,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"kpi2lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"1,204","subtext":"+3.4%","subtextVariant":"metric","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"kpi2rhs"}},"partial":true,"hasDynamicProps":false,"statementId":"kpi2"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"churn","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"user-minus","category":"people"},"partial":true,"hasDynamicProps":false,"statementId":"kpi3icon"},"iconVariant":"neutral","iconSize":"m","title":"Churn","subtitle":"This month","bold":false,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"kpi3lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"1.8%","subtext":"-0.3%","subtextVariant":"metric","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"kpi3rhs"}},"partial":true,"hasDynamicProps":false,"statementId":"kpi3"}],"layout":"grid","responsive":true},"partial":true,"hasDynamicProps":false,"statementId":"kpiBlock"},{"type":"element","typeName":"InlineHeader","props":{"heading":"Top Products","description":"By units sold"},"partial":true,"hasDynamicProps":false,"statementId":"productsHeader"},{"type":"element","typeName":"CompositeCardBlock","props":{"items":[{"type":"element","typeName":"CompositeCardItem","props":{"id":"widget-pro","header":null,"body":[],"footer":null},"partial":true,"hasDynamicProps":false,"statementId":"p1"}],"layout":"grid","responsive":true},"partial":true,"hasDynamicProps":false,"statementId":"productCards"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["p1icon","p1body","p1footer","p2"],"orphaned":[],"statementCount":19,"errors":[{"code":"null-required","component":"IconText","path":"/icon","message":"required field \"/icon\" cannot be null — signature: IconText(icon*: Icon, iconVariant*: \"neutral\"|\"info\"|\"success\"|\"warning\"|\"danger\"|\"inverted\"|\"filled\"|\"soft\", iconSize*: \"xs\"|\"s\"|\"m\"|\"l\"|\"xl\"|\"sm\"|\"md\"|\"lg\", title*: string, subtitle: string, bold*: boolean, layout*: \"horizontal\"|\"vertical\")","statementId":"p1header"},{"code":"missing-required","component":"IconText","path":"/title","message":"missing required field \"/title\" — signature: IconText(icon*: Icon, iconVariant*: \"neutral\"|\"info\"|\"success\"|\"warning\"|\"danger\"|\"inverted\"|\"filled\"|\"soft\", iconSize*: \"xs\"|\"s\"|\"m\"|\"l\"|\"xl\"|\"sm\"|\"md\"|\"lg\", title*: string, subtitle: string, bold*: boolean, layout*: \"horizontal\"|\"vertical\")","statementId":"p1header"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":1312,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"InlineHeader","props":{"heading":"This Month","description":"Key account metrics"},"partial":true,"hasDynamicProps":false,"statementId":"kpiHeader"},{"type":"element","typeName":"SnippetCardBlock","props":{"items":[{"type":"element","typeName":"SnippetCardItem","props":{"id":"revenue","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"circle-dollar-sign","category":"finance"},"partial":true,"hasDynamicProps":false,"statementId":"kpi1icon"},"iconVariant":"neutral","iconSize":"m","title":"Revenue","subtitle":"Month to date","bold":false,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"kpi1lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"$48,200","subtext":"+8.1%","subtextVariant":"metric","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"kpi1rhs"}},"partial":true,"hasDynamicProps":false,"statementId":"kpi1"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"active-users","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"users","category":"people"},"partial":true,"hasDynamicProps":false,"statementId":"kpi2icon"},"iconVariant":"neutral","iconSize":"m","title":"Active Users","subtitle":"This month","bold":false,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"kpi2lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"1,204","subtext":"+3.4%","subtextVariant":"metric","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"kpi2rhs"}},"partial":true,"hasDynamicProps":false,"statementId":"kpi2"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"churn","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"user-minus","category":"people"},"partial":true,"hasDynamicProps":false,"statementId":"kpi3icon"},"iconVariant":"neutral","iconSize":"m","title":"Churn","subtitle":"This month","bold":false,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"kpi3lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"1.8%","subtext":"-0.3%","subtextVariant":"metric","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"kpi3rhs"}},"partial":true,"hasDynamicProps":false,"statementId":"kpi3"}],"layout":"grid","responsive":true},"partial":true,"hasDynamicProps":false,"statementId":"kpiBlock"},{"type":"element","typeName":"InlineHeader","props":{"heading":"Top Products","description":"By units sold"},"partial":true,"hasDynamicProps":false,"statementId":"productsHeader"},{"type":"element","typeName":"CompositeCardBlock","props":{"items":[{"type":"element","typeName":"CompositeCardItem","props":{"id":"widget-pro","header":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"package","category":"shopping"},"partial":true,"hasDynamicProps":false,"statementId":"p1icon"},"iconVariant":"neutral","iconSize":"m","title":"Widget Pro","subtitle":"Best seller","bold":true,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"p1header"},"body":[{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"2,410 units","subtext":"Sold this month"},"partial":true,"hasDynamicProps":false,"statementId":"p1body"}],"footer":null},"partial":true,"hasDynamicProps":false,"statementId":"p1"}],"layout":"grid","responsive":true},"partial":true,"hasDynamicProps":false,"statementId":"productCards"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["p1footer","p2"],"orphaned":[],"statementCount":21,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":1476,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"InlineHeader","props":{"heading":"This Month","description":"Key account metrics"},"partial":true,"hasDynamicProps":false,"statementId":"kpiHeader"},{"type":"element","typeName":"SnippetCardBlock","props":{"items":[{"type":"element","typeName":"SnippetCardItem","props":{"id":"revenue","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"circle-dollar-sign","category":"finance"},"partial":true,"hasDynamicProps":false,"statementId":"kpi1icon"},"iconVariant":"neutral","iconSize":"m","title":"Revenue","subtitle":"Month to date","bold":false,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"kpi1lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"$48,200","subtext":"+8.1%","subtextVariant":"metric","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"kpi1rhs"}},"partial":true,"hasDynamicProps":false,"statementId":"kpi1"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"active-users","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"users","category":"people"},"partial":true,"hasDynamicProps":false,"statementId":"kpi2icon"},"iconVariant":"neutral","iconSize":"m","title":"Active Users","subtitle":"This month","bold":false,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"kpi2lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"1,204","subtext":"+3.4%","subtextVariant":"metric","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"kpi2rhs"}},"partial":true,"hasDynamicProps":false,"statementId":"kpi2"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"churn","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"user-minus","category":"people"},"partial":true,"hasDynamicProps":false,"statementId":"kpi3icon"},"iconVariant":"neutral","iconSize":"m","title":"Churn","subtitle":"This month","bold":false,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"kpi3lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"1.8%","subtext":"-0.3%","subtextVariant":"metric","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"kpi3rhs"}},"partial":true,"hasDynamicProps":false,"statementId":"kpi3"}],"layout":"grid","responsive":true},"partial":true,"hasDynamicProps":false,"statementId":"kpiBlock"},{"type":"element","typeName":"InlineHeader","props":{"heading":"Top Products","description":"By units sold"},"partial":true,"hasDynamicProps":false,"statementId":"productsHeader"},{"type":"element","typeName":"CompositeCardBlock","props":{"items":[{"type":"element","typeName":"CompositeCardItem","props":{"id":"widget-pro","header":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"package","category":"shopping"},"partial":true,"hasDynamicProps":false,"statementId":"p1icon"},"iconVariant":"neutral","iconSize":"m","title":"Widget Pro","subtitle":"Best seller","bold":true,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"p1header"},"body":[{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"2,410 units","subtext":"Sold this month","trend":{"direction":"up","value":12}},"partial":true,"hasDynamicProps":false,"statementId":"p1body"}],"footer":{"price":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"$29.00","subtextVariant":"text","size":"sm"},"partial":true,"hasDynamicProps":false},"button":{"type":"element","typeName":"Button","props":{"label":"View Details","action":{"k":"Comp","name":"Action","args":[{"k":"Arr","els":[{"k":"Comp","name":"ToAssistant","args":[{"k":"Str","v":"Show details"}]}]}]}},"partial":true,"hasDynamicProps":true,"statementId":"p1btn"}}},"partial":true,"hasDynamicProps":true,"statementId":"p1"}],"layout":"grid","responsive":true},"partial":true,"hasDynamicProps":true,"statementId":"productCards"}]},"partial":true,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["p2"],"orphaned":[],"statementCount":23,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":1640,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"InlineHeader","props":{"heading":"This Month","description":"Key account metrics"},"partial":true,"hasDynamicProps":false,"statementId":"kpiHeader"},{"type":"element","typeName":"SnippetCardBlock","props":{"items":[{"type":"element","typeName":"SnippetCardItem","props":{"id":"revenue","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"circle-dollar-sign","category":"finance"},"partial":true,"hasDynamicProps":false,"statementId":"kpi1icon"},"iconVariant":"neutral","iconSize":"m","title":"Revenue","subtitle":"Month to date","bold":false,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"kpi1lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"$48,200","subtext":"+8.1%","subtextVariant":"metric","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"kpi1rhs"}},"partial":true,"hasDynamicProps":false,"statementId":"kpi1"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"active-users","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"users","category":"people"},"partial":true,"hasDynamicProps":false,"statementId":"kpi2icon"},"iconVariant":"neutral","iconSize":"m","title":"Active Users","subtitle":"This month","bold":false,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"kpi2lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"1,204","subtext":"+3.4%","subtextVariant":"metric","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"kpi2rhs"}},"partial":true,"hasDynamicProps":false,"statementId":"kpi2"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"churn","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"user-minus","category":"people"},"partial":true,"hasDynamicProps":false,"statementId":"kpi3icon"},"iconVariant":"neutral","iconSize":"m","title":"Churn","subtitle":"This month","bold":false,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"kpi3lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"1.8%","subtext":"-0.3%","subtextVariant":"metric","size":"sm"},"partial":true,"hasDynamicProps":false,"statementId":"kpi3rhs"}},"partial":true,"hasDynamicProps":false,"statementId":"kpi3"}],"layout":"grid","responsive":true},"partial":true,"hasDynamicProps":false,"statementId":"kpiBlock"},{"type":"element","typeName":"InlineHeader","props":{"heading":"Top Products","description":"By units sold"},"partial":true,"hasDynamicProps":false,"statementId":"productsHeader"},{"type":"element","typeName":"CompositeCardBlock","props":{"items":[{"type":"element","typeName":"CompositeCardItem","props":{"id":"widget-pro","header":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"package","category":"shopping"},"partial":true,"hasDynamicProps":false,"statementId":"p1icon"},"iconVariant":"neutral","iconSize":"m","title":"Widget Pro","subtitle":"Best seller","bold":true,"layout":"horizontal"},"partial":true,"hasDynamicProps":false,"statementId":"p1header"},"body":[{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"2,410 units","subtext":"Sold this month","trend":{"direction":"up","value":12}},"partial":true,"hasDynamicProps":false,"statementId":"p1body"}],"footer":{"price":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"$29.00","subtextVariant":"text","size":"sm"},"partial":true,"hasDynamicProps":false},"button":{"type":"element","typeName":"Button","props":{"label":"View Details","action":{"k":"Comp","name":"Action","args":[{"k":"Arr","els":[{"k":"Comp","name":"ToAssistant","args":[{"k":"Str","v":"Show details for Widget Pro"}]}]}]},"variant":"secondary"},"partial":true,"hasDynamicProps":true,"statementId":"p1btn"}}},"partial":true,"hasDynamicProps":true,"statementId":"p1"},{"type":"element","typeName":"CompositeCardItem","props":{"id":"widget-mini","header":null,"body":[],"footer":null},"partial":true,"hasDynamicProps":false,"statementId":"p2"}],"layout":"grid","responsive":true},"partial":true,"hasDynamicProps":true,"statementId":"productCards"}]},"partial":true,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["p2icon","p2body","p2footer"],"orphaned":[],"statementCount":25,"errors":[{"code":"null-required","component":"IconText","path":"/icon","message":"required field \"/icon\" cannot be null — signature: IconText(icon*: Icon, iconVariant*: \"neutral\"|\"info\"|\"success\"|\"warning\"|\"danger\"|\"inverted\"|\"filled\"|\"soft\", iconSize*: \"xs\"|\"s\"|\"m\"|\"l\"|\"xl\"|\"sm\"|\"md\"|\"lg\", title*: string, subtitle: string, bold*: boolean, layout*: \"horizontal\"|\"vertical\")","statementId":"p2header"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":1804,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"InlineHeader","props":{"heading":"This Month","description":"Key account metrics"},"partial":false,"hasDynamicProps":false,"statementId":"kpiHeader"},{"type":"element","typeName":"SnippetCardBlock","props":{"items":[{"type":"element","typeName":"SnippetCardItem","props":{"id":"revenue","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"circle-dollar-sign","category":"finance"},"partial":false,"hasDynamicProps":false,"statementId":"kpi1icon"},"iconVariant":"neutral","iconSize":"m","title":"Revenue","subtitle":"Month to date","bold":false,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"kpi1lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"$48,200","subtext":"+8.1%","subtextVariant":"metric","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"kpi1rhs"}},"partial":false,"hasDynamicProps":false,"statementId":"kpi1"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"active-users","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"users","category":"people"},"partial":false,"hasDynamicProps":false,"statementId":"kpi2icon"},"iconVariant":"neutral","iconSize":"m","title":"Active Users","subtitle":"This month","bold":false,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"kpi2lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"1,204","subtext":"+3.4%","subtextVariant":"metric","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"kpi2rhs"}},"partial":false,"hasDynamicProps":false,"statementId":"kpi2"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"churn","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"user-minus","category":"people"},"partial":false,"hasDynamicProps":false,"statementId":"kpi3icon"},"iconVariant":"neutral","iconSize":"m","title":"Churn","subtitle":"This month","bold":false,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"kpi3lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"1.8%","subtext":"-0.3%","subtextVariant":"metric","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"kpi3rhs"}},"partial":false,"hasDynamicProps":false,"statementId":"kpi3"}],"layout":"grid","responsive":true},"partial":false,"hasDynamicProps":false,"statementId":"kpiBlock"},{"type":"element","typeName":"InlineHeader","props":{"heading":"Top Products","description":"By units sold"},"partial":false,"hasDynamicProps":false,"statementId":"productsHeader"},{"type":"element","typeName":"CompositeCardBlock","props":{"items":[{"type":"element","typeName":"CompositeCardItem","props":{"id":"widget-pro","header":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"package","category":"shopping"},"partial":false,"hasDynamicProps":false,"statementId":"p1icon"},"iconVariant":"neutral","iconSize":"m","title":"Widget Pro","subtitle":"Best seller","bold":true,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"p1header"},"body":[{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"2,410 units","subtext":"Sold this month","trend":{"direction":"up","value":12}},"partial":false,"hasDynamicProps":false,"statementId":"p1body"}],"footer":{"price":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"$29.00","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false},"button":{"type":"element","typeName":"Button","props":{"label":"View Details","action":{"k":"Comp","name":"Action","args":[{"k":"Arr","els":[{"k":"Comp","name":"ToAssistant","args":[{"k":"Str","v":"Show details for Widget Pro"}]}]}]},"variant":"secondary"},"partial":false,"hasDynamicProps":true,"statementId":"p1btn"}}},"partial":false,"hasDynamicProps":true,"statementId":"p1"},{"type":"element","typeName":"CompositeCardItem","props":{"id":"widget-mini","header":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"box","category":"shopping"},"partial":false,"hasDynamicProps":false,"statementId":"p2icon"},"iconVariant":"neutral","iconSize":"m","title":"Widget Mini","subtitle":"New arrival","bold":true,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"p2header"},"body":[{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"980 units","subtext":"Sold this month","trend":{"direction":"up","value":4}},"partial":false,"hasDynamicProps":false,"statementId":"p2body"}],"footer":null},"partial":false,"hasDynamicProps":false,"statementId":"p2"}],"layout":"grid","responsive":true},"partial":false,"hasDynamicProps":true,"statementId":"productCards"}]},"partial":false,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["p2footer"],"orphaned":[],"statementCount":27,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":1962,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"InlineHeader","props":{"heading":"This Month","description":"Key account metrics"},"partial":false,"hasDynamicProps":false,"statementId":"kpiHeader"},{"type":"element","typeName":"SnippetCardBlock","props":{"items":[{"type":"element","typeName":"SnippetCardItem","props":{"id":"revenue","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"circle-dollar-sign","category":"finance"},"partial":false,"hasDynamicProps":false,"statementId":"kpi1icon"},"iconVariant":"neutral","iconSize":"m","title":"Revenue","subtitle":"Month to date","bold":false,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"kpi1lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"$48,200","subtext":"+8.1%","subtextVariant":"metric","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"kpi1rhs"}},"partial":false,"hasDynamicProps":false,"statementId":"kpi1"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"active-users","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"users","category":"people"},"partial":false,"hasDynamicProps":false,"statementId":"kpi2icon"},"iconVariant":"neutral","iconSize":"m","title":"Active Users","subtitle":"This month","bold":false,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"kpi2lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"1,204","subtext":"+3.4%","subtextVariant":"metric","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"kpi2rhs"}},"partial":false,"hasDynamicProps":false,"statementId":"kpi2"},{"type":"element","typeName":"SnippetCardItem","props":{"id":"churn","lhs":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"user-minus","category":"people"},"partial":false,"hasDynamicProps":false,"statementId":"kpi3icon"},"iconVariant":"neutral","iconSize":"m","title":"Churn","subtitle":"This month","bold":false,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"kpi3lhs"},"rhs":{"type":"element","typeName":"BoldText","props":{"variant":"number","value":"1.8%","subtext":"-0.3%","subtextVariant":"metric","size":"sm"},"partial":false,"hasDynamicProps":false,"statementId":"kpi3rhs"}},"partial":false,"hasDynamicProps":false,"statementId":"kpi3"}],"layout":"grid","responsive":true},"partial":false,"hasDynamicProps":false,"statementId":"kpiBlock"},{"type":"element","typeName":"InlineHeader","props":{"heading":"Top Products","description":"By units sold"},"partial":false,"hasDynamicProps":false,"statementId":"productsHeader"},{"type":"element","typeName":"CompositeCardBlock","props":{"items":[{"type":"element","typeName":"CompositeCardItem","props":{"id":"widget-pro","header":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"package","category":"shopping"},"partial":false,"hasDynamicProps":false,"statementId":"p1icon"},"iconVariant":"neutral","iconSize":"m","title":"Widget Pro","subtitle":"Best seller","bold":true,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"p1header"},"body":[{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"2,410 units","subtext":"Sold this month","trend":{"direction":"up","value":12}},"partial":false,"hasDynamicProps":false,"statementId":"p1body"}],"footer":{"price":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"$29.00","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false},"button":{"type":"element","typeName":"Button","props":{"label":"View Details","action":{"k":"Comp","name":"Action","args":[{"k":"Arr","els":[{"k":"Comp","name":"ToAssistant","args":[{"k":"Str","v":"Show details for Widget Pro"}]}]}]},"variant":"secondary"},"partial":false,"hasDynamicProps":true,"statementId":"p1btn"}}},"partial":false,"hasDynamicProps":true,"statementId":"p1"},{"type":"element","typeName":"CompositeCardItem","props":{"id":"widget-mini","header":{"type":"element","typeName":"IconText","props":{"icon":{"type":"element","typeName":"Icon","props":{"name":"box","category":"shopping"},"partial":false,"hasDynamicProps":false,"statementId":"p2icon"},"iconVariant":"neutral","iconSize":"m","title":"Widget Mini","subtitle":"New arrival","bold":true,"layout":"horizontal"},"partial":false,"hasDynamicProps":false,"statementId":"p2header"},"body":[{"type":"element","typeName":"MetricIndicatorInline","props":{"value":"980 units","subtext":"Sold this month","trend":{"direction":"up","value":4}},"partial":false,"hasDynamicProps":false,"statementId":"p2body"}],"footer":{"price":{"type":"element","typeName":"BoldText","props":{"variant":"text","value":"$14.00","subtextVariant":"text","size":"sm"},"partial":false,"hasDynamicProps":false},"button":{"type":"element","typeName":"Button","props":{"label":"View Details","action":{"k":"Comp","name":"Action","args":[{"k":"Arr","els":[{"k":"Comp","name":"ToAssistant","args":[{"k":"Str","v":"Show details for Widget Mini"}]}]}]},"variant":"secondary"},"partial":false,"hasDynamicProps":true,"statementId":"p2btn"}}},"partial":false,"hasDynamicProps":true,"statementId":"p2"}],"layout":"grid","responsive":true},"partial":false,"hasDynamicProps":true,"statementId":"productCards"}]},"partial":false,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":29,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"openui-example-6","schema":"openui","input":"Example 6 — Editable table and a selection form:\n\nroot = Stack([tableHeader, editTable, formHeader, prefsForm])\ntableHeader = InlineHeader(\"Team Roster\", \"Click a cell to edit\")\neditTable = EditableTable(\"roster\", [colName, colRole, colStart], [row1, row2])\ncolName = { type: \"text\", key: \"name\", header: \"Name\" }\ncolRole = { type: \"select\", key: \"role\", header: \"Role\", options: [roleEng, roleDesign] }\nroleEng = { value: \"eng\", label: \"Engineering\" }\nroleDesign = { value: \"design\", label: \"Design\" }\ncolStart = { type: \"date-single\", key: \"start\", header: \"Start Date\" }\nrow1 = { id: \"1\", values: [\"Alex Kim\", \"eng\", \"2024-01-15\"] }\nrow2 = { id: \"2\", values: [\"Jamie Lee\", \"design\", \"2024-03-02\"] }\nformHeader = InlineHeader(\"Preferences\", \"Tell us how you like to work\")\nprefsForm = Form(\"prefs\", formButtons, [fc1, fc2])\nfc1 = FormControl(\"Working Style\", styleCards, \"Pick the one that fits best\")\nstyleCards = OptionCards(\"style\", \"single\", [style1, style2])\nstyle1 = OptionCard(\"focused\", \"Deep Focus\", \"Long uninterrupted blocks\", style1icon)\nstyle1icon = Icon(\"target\", \"tools\")\nstyle2 = OptionCard(\"collab\", \"Collaborative\", \"Frequent pairing and syncs\", style2icon)\nstyle2icon = Icon(\"users\", \"people\")\nfc2 = FormControl(\"Tools\", toolChips, \"Select all that apply\")\ntoolChips = Chips(\"tools\", \"multiple\", [tool1, tool2, tool3])\ntool1 = ChipItem(\"figma\", \"Figma\", tool1icon)\ntool1icon = Icon(\"figma\", \"design\")\ntool2 = ChipItem(\"slack\", \"Slack\", tool2icon)\ntool2icon = Icon(\"message-square\", \"communication\")\ntool3 = ChipItem(\"notion\", \"Notion\", tool3icon)\ntool3icon = Icon(\"notebook\", \"text\")\nformButtons = Buttons([Button(\"Save\", Action([@ToAssistant(\"Save preferences\")]), \"primary\")])","checkpoints":[{"at":142,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"InlineHeader","props":{"heading":"Te"},"partial":true,"hasDynamicProps":false,"statementId":"tableHeader"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["editTable","formHeader","prefsForm"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":284,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"InlineHeader","props":{"heading":"Team Roster","description":"Click a cell to edit"},"partial":true,"hasDynamicProps":false,"statementId":"tableHeader"},{"type":"element","typeName":"EditableTable","props":{"name":"roster","columns":[{"type":"text"}],"data":[]},"partial":true,"hasDynamicProps":false,"statementId":"editTable"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["colRole","colStart","row1","row2","formHeader","prefsForm"],"orphaned":[],"statementCount":4,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":426,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"InlineHeader","props":{"heading":"Team Roster","description":"Click a cell to edit"},"partial":true,"hasDynamicProps":false,"statementId":"tableHeader"},{"type":"element","typeName":"EditableTable","props":{"name":"roster","columns":[{"type":"text","key":"name","header":"Name"},{"type":"select","key":"role","header":"Role","options":[{"value":"en"}]}],"data":[]},"partial":true,"hasDynamicProps":false,"statementId":"editTable"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["roleDesign","colStart","row1","row2","formHeader","prefsForm"],"orphaned":[],"statementCount":6,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":568,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"InlineHeader","props":{"heading":"Team Roster","description":"Click a cell to edit"},"partial":true,"hasDynamicProps":false,"statementId":"tableHeader"},{"type":"element","typeName":"EditableTable","props":{"name":"roster","columns":[{"type":"text","key":"name","header":"Name"},{"type":"select","key":"role","header":"Role","options":[{"value":"eng","label":"Engineering"},{"value":"design","label":"Design"}]},{"type":"date-single","key":"start","header":"Start Da"}],"data":[]},"partial":true,"hasDynamicProps":false,"statementId":"editTable"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["row1","row2","formHeader","prefsForm"],"orphaned":[],"statementCount":8,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":710,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"InlineHeader","props":{"heading":"Team Roster","description":"Click a cell to edit"},"partial":false,"hasDynamicProps":false,"statementId":"tableHeader"},{"type":"element","typeName":"EditableTable","props":{"name":"roster","columns":[{"type":"text","key":"name","header":"Name"},{"type":"select","key":"role","header":"Role","options":[{"value":"eng","label":"Engineering"},{"value":"design","label":"Design"}]},{"type":"date-single","key":"start","header":"Start Date"}],"data":[{"id":"1","values":["Alex Kim","eng","2024-01-15"]},{"id":"2","values":["Jamie Lee","design","2024-03-02"]}]},"partial":false,"hasDynamicProps":false,"statementId":"editTable"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["formHeader","prefsForm"],"orphaned":[],"statementCount":10,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":852,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"InlineHeader","props":{"heading":"Team Roster","description":"Click a cell to edit"},"partial":true,"hasDynamicProps":false,"statementId":"tableHeader"},{"type":"element","typeName":"EditableTable","props":{"name":"roster","columns":[{"type":"text","key":"name","header":"Name"},{"type":"select","key":"role","header":"Role","options":[{"value":"eng","label":"Engineering"},{"value":"design","label":"Design"}]},{"type":"date-single","key":"start","header":"Start Date"}],"data":[{"id":"1","values":["Alex Kim","eng","2024-01-15"]},{"id":"2","values":["Jamie Lee","design","2024-03-02"]}]},"partial":true,"hasDynamicProps":false,"statementId":"editTable"},{"type":"element","typeName":"InlineHeader","props":{"heading":"Preferences","description":"Tell us how you like to work"},"partial":true,"hasDynamicProps":false,"statementId":"formHeader"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["formButtons","fc2"],"orphaned":[],"statementCount":13,"errors":[{"code":"missing-required","component":"FormControl","path":"/input","message":"missing required field \"/input\" — signature: FormControl(label*: string, input*, hint: string)","statementId":"fc1"},{"code":"null-required","component":"Form","path":"/buttons","message":"required field \"/buttons\" cannot be null — signature: Form(name*: string, buttons*: Buttons, fields*: array)","statementId":"prefsForm"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":994,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"InlineHeader","props":{"heading":"Team Roster","description":"Click a cell to edit"},"partial":true,"hasDynamicProps":false,"statementId":"tableHeader"},{"type":"element","typeName":"EditableTable","props":{"name":"roster","columns":[{"type":"text","key":"name","header":"Name"},{"type":"select","key":"role","header":"Role","options":[{"value":"eng","label":"Engineering"},{"value":"design","label":"Design"}]},{"type":"date-single","key":"start","header":"Start Date"}],"data":[{"id":"1","values":["Alex Kim","eng","2024-01-15"]},{"id":"2","values":["Jamie Lee","design","2024-03-02"]}]},"partial":true,"hasDynamicProps":false,"statementId":"editTable"},{"type":"element","typeName":"InlineHeader","props":{"heading":"Preferences","description":"Tell us how you like to work"},"partial":true,"hasDynamicProps":false,"statementId":"formHeader"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["formButtons","style2","fc2"],"orphaned":[],"statementCount":15,"errors":[{"code":"missing-required","component":"OptionCard","path":"/title","message":"missing required field \"/title\" — signature: OptionCard(value*: string, title*: string, subtitle: string, topContent, disabled: boolean)","statementId":"style1"},{"code":"null-required","component":"Form","path":"/buttons","message":"required field \"/buttons\" cannot be null — signature: Form(name*: string, buttons*: Buttons, fields*: array)","statementId":"prefsForm"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":1136,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"InlineHeader","props":{"heading":"Team Roster","description":"Click a cell to edit"},"partial":true,"hasDynamicProps":false,"statementId":"tableHeader"},{"type":"element","typeName":"EditableTable","props":{"name":"roster","columns":[{"type":"text","key":"name","header":"Name"},{"type":"select","key":"role","header":"Role","options":[{"value":"eng","label":"Engineering"},{"value":"design","label":"Design"}]},{"type":"date-single","key":"start","header":"Start Date"}],"data":[{"id":"1","values":["Alex Kim","eng","2024-01-15"]},{"id":"2","values":["Jamie Lee","design","2024-03-02"]}]},"partial":true,"hasDynamicProps":false,"statementId":"editTable"},{"type":"element","typeName":"InlineHeader","props":{"heading":"Preferences","description":"Tell us how you like to work"},"partial":true,"hasDynamicProps":false,"statementId":"formHeader"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["formButtons","fc2"],"orphaned":[],"statementCount":17,"errors":[{"code":"null-required","component":"Form","path":"/buttons","message":"required field \"/buttons\" cannot be null — signature: Form(name*: string, buttons*: Buttons, fields*: array)","statementId":"prefsForm"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":1278,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"InlineHeader","props":{"heading":"Team Roster","description":"Click a cell to edit"},"partial":false,"hasDynamicProps":false,"statementId":"tableHeader"},{"type":"element","typeName":"EditableTable","props":{"name":"roster","columns":[{"type":"text","key":"name","header":"Name"},{"type":"select","key":"role","header":"Role","options":[{"value":"eng","label":"Engineering"},{"value":"design","label":"Design"}]},{"type":"date-single","key":"start","header":"Start Date"}],"data":[{"id":"1","values":["Alex Kim","eng","2024-01-15"]},{"id":"2","values":["Jamie Lee","design","2024-03-02"]}]},"partial":false,"hasDynamicProps":false,"statementId":"editTable"},{"type":"element","typeName":"InlineHeader","props":{"heading":"Preferences","description":"Tell us how you like to work"},"partial":false,"hasDynamicProps":false,"statementId":"formHeader"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["formButtons","toolChips"],"orphaned":[],"statementCount":19,"errors":[{"code":"null-required","component":"FormControl","path":"/input","message":"required field \"/input\" cannot be null — signature: FormControl(label*: string, input*, hint: string)","statementId":"fc2"},{"code":"null-required","component":"Form","path":"/buttons","message":"required field \"/buttons\" cannot be null — signature: Form(name*: string, buttons*: Buttons, fields*: array)","statementId":"prefsForm"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":1420,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"InlineHeader","props":{"heading":"Team Roster","description":"Click a cell to edit"},"partial":true,"hasDynamicProps":false,"statementId":"tableHeader"},{"type":"element","typeName":"EditableTable","props":{"name":"roster","columns":[{"type":"text","key":"name","header":"Name"},{"type":"select","key":"role","header":"Role","options":[{"value":"eng","label":"Engineering"},{"value":"design","label":"Design"}]},{"type":"date-single","key":"start","header":"Start Date"}],"data":[{"id":"1","values":["Alex Kim","eng","2024-01-15"]},{"id":"2","values":["Jamie Lee","design","2024-03-02"]}]},"partial":true,"hasDynamicProps":false,"statementId":"editTable"},{"type":"element","typeName":"InlineHeader","props":{"heading":"Preferences","description":"Tell us how you like to work"},"partial":true,"hasDynamicProps":false,"statementId":"formHeader"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["formButtons","tool2","tool3"],"orphaned":[],"statementCount":22,"errors":[{"code":"null-required","component":"Form","path":"/buttons","message":"required field \"/buttons\" cannot be null — signature: Form(name*: string, buttons*: Buttons, fields*: array)","statementId":"prefsForm"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":1562,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"InlineHeader","props":{"heading":"Team Roster","description":"Click a cell to edit"},"partial":true,"hasDynamicProps":false,"statementId":"tableHeader"},{"type":"element","typeName":"EditableTable","props":{"name":"roster","columns":[{"type":"text","key":"name","header":"Name"},{"type":"select","key":"role","header":"Role","options":[{"value":"eng","label":"Engineering"},{"value":"design","label":"Design"}]},{"type":"date-single","key":"start","header":"Start Date"}],"data":[{"id":"1","values":["Alex Kim","eng","2024-01-15"]},{"id":"2","values":["Jamie Lee","design","2024-03-02"]}]},"partial":true,"hasDynamicProps":false,"statementId":"editTable"},{"type":"element","typeName":"InlineHeader","props":{"heading":"Preferences","description":"Tell us how you like to work"},"partial":true,"hasDynamicProps":false,"statementId":"formHeader"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["formButtons","tool3"],"orphaned":[],"statementCount":25,"errors":[{"code":"null-required","component":"Form","path":"/buttons","message":"required field \"/buttons\" cannot be null — signature: Form(name*: string, buttons*: Buttons, fields*: array)","statementId":"prefsForm"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":1699,"expected":{"root":{"type":"element","typeName":"Stack","props":{"children":[{"type":"element","typeName":"InlineHeader","props":{"heading":"Team Roster","description":"Click a cell to edit"},"partial":false,"hasDynamicProps":false,"statementId":"tableHeader"},{"type":"element","typeName":"EditableTable","props":{"name":"roster","columns":[{"type":"text","key":"name","header":"Name"},{"type":"select","key":"role","header":"Role","options":[{"value":"eng","label":"Engineering"},{"value":"design","label":"Design"}]},{"type":"date-single","key":"start","header":"Start Date"}],"data":[{"id":"1","values":["Alex Kim","eng","2024-01-15"]},{"id":"2","values":["Jamie Lee","design","2024-03-02"]}]},"partial":false,"hasDynamicProps":false,"statementId":"editTable"},{"type":"element","typeName":"InlineHeader","props":{"heading":"Preferences","description":"Tell us how you like to work"},"partial":false,"hasDynamicProps":false,"statementId":"formHeader"},{"type":"element","typeName":"Form","props":{"name":"prefs","buttons":{"type":"element","typeName":"Buttons","props":{"buttons":[{"type":"element","typeName":"Button","props":{"label":"Save","action":{"k":"Comp","name":"Action","args":[{"k":"Arr","els":[{"k":"Comp","name":"ToAssistant","args":[{"k":"Str","v":"Save preferences"}]}]}]},"variant":"primary"},"partial":false,"hasDynamicProps":true}]},"partial":false,"hasDynamicProps":true,"statementId":"formButtons"},"fields":[{"type":"element","typeName":"FormControl","props":{"label":"Working Style","input":{"type":"element","typeName":"OptionCards","props":{"name":"style","type":"single","items":[{"type":"element","typeName":"OptionCard","props":{"value":"focused","title":"Deep Focus","subtitle":"Long uninterrupted blocks","topContent":{"type":"element","typeName":"Icon","props":{"name":"target","category":"tools"},"partial":false,"hasDynamicProps":false,"statementId":"style1icon"}},"partial":false,"hasDynamicProps":false,"statementId":"style1"},{"type":"element","typeName":"OptionCard","props":{"value":"collab","title":"Collaborative","subtitle":"Frequent pairing and syncs","topContent":{"type":"element","typeName":"Icon","props":{"name":"users","category":"people"},"partial":false,"hasDynamicProps":false,"statementId":"style2icon"}},"partial":false,"hasDynamicProps":false,"statementId":"style2"}]},"partial":false,"hasDynamicProps":false,"statementId":"styleCards"},"hint":"Pick the one that fits best"},"partial":false,"hasDynamicProps":false,"statementId":"fc1"},{"type":"element","typeName":"FormControl","props":{"label":"Tools","input":{"type":"element","typeName":"Chips","props":{"name":"tools","type":"multiple","items":[{"type":"element","typeName":"ChipItem","props":{"value":"figma","label":"Figma","icon":{"type":"element","typeName":"Icon","props":{"name":"figma","category":"design"},"partial":false,"hasDynamicProps":false,"statementId":"tool1icon"}},"partial":false,"hasDynamicProps":false,"statementId":"tool1"},{"type":"element","typeName":"ChipItem","props":{"value":"slack","label":"Slack","icon":{"type":"element","typeName":"Icon","props":{"name":"message-square","category":"communication"},"partial":false,"hasDynamicProps":false,"statementId":"tool2icon"}},"partial":false,"hasDynamicProps":false,"statementId":"tool2"},{"type":"element","typeName":"ChipItem","props":{"value":"notion","label":"Notion","icon":{"type":"element","typeName":"Icon","props":{"name":"notebook","category":"text"},"partial":false,"hasDynamicProps":false,"statementId":"tool3icon"}},"partial":false,"hasDynamicProps":false,"statementId":"tool3"}]},"partial":false,"hasDynamicProps":false,"statementId":"toolChips"},"hint":"Select all that apply"},"partial":false,"hasDynamicProps":false,"statementId":"fc2"}]},"partial":false,"hasDynamicProps":true,"statementId":"prefsForm"}]},"partial":false,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":27,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"basic","schema":"test","input":"root = Card([t1, b1])\nt1 = Text(\"Hi\")\nb1 = Button(\"Go\")","checkpoints":[{"at":5,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":10,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":["Car"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":15,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["t1"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":20,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["t1","b1"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":25,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["t1","b1"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":30,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["Tex","b1"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":35,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"Hi"},"partial":true,"hasDynamicProps":false,"statementId":"t1"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["b1"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":40,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"Hi"},"partial":false,"hasDynamicProps":false,"statementId":"t1"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["b1"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":45,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"Hi"},"partial":false,"hasDynamicProps":false,"statementId":"t1"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["Bu"],"orphaned":[],"statementCount":3,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":50,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"Hi"},"partial":true,"hasDynamicProps":false,"statementId":"t1"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":3,"errors":[{"code":"missing-required","component":"Button","path":"/label","message":"missing required field \"/label\" — signature: Button(label*: string, action, variant: \"primary\"|\"secondary\")","statementId":"b1"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":55,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"Hi"},"partial":false,"hasDynamicProps":false,"statementId":"t1"},{"type":"element","typeName":"Button","props":{"label":"Go"},"partial":false,"hasDynamicProps":false,"statementId":"b1"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":3,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"missing-required","schema":"test","input":"root = Card([t1, b1])\nt1 = Text()\nb1 = Button(\"Go\")","checkpoints":[{"at":5,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":10,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":["Car"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":15,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["t1"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":20,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["t1","b1"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":25,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["t1","b1"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":30,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["Tex","b1"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":35,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["b1"],"orphaned":[],"statementCount":2,"errors":[{"code":"missing-required","component":"Text","path":"/text","message":"missing required field \"/text\" — signature: Text(text*: string, size: \"s\"|\"m\"|\"l\")","statementId":"t1"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":40,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["B"],"orphaned":[],"statementCount":3,"errors":[{"code":"missing-required","component":"Text","path":"/text","message":"missing required field \"/text\" — signature: Text(text*: string, size: \"s\"|\"m\"|\"l\")","statementId":"t1"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":45,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["Button"],"orphaned":[],"statementCount":3,"errors":[{"code":"missing-required","component":"Text","path":"/text","message":"missing required field \"/text\" — signature: Text(text*: string, size: \"s\"|\"m\"|\"l\")","statementId":"t1"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":50,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Button","props":{"label":"Go"},"partial":true,"hasDynamicProps":false,"statementId":"b1"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":3,"errors":[{"code":"missing-required","component":"Text","path":"/text","message":"missing required field \"/text\" — signature: Text(text*: string, size: \"s\"|\"m\"|\"l\")","statementId":"t1"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":51,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Button","props":{"label":"Go"},"partial":false,"hasDynamicProps":false,"statementId":"b1"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":3,"errors":[{"code":"missing-required","component":"Text","path":"/text","message":"missing required field \"/text\" — signature: Text(text*: string, size: \"s\"|\"m\"|\"l\")","statementId":"t1"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"null-required-with-default","schema":"test","input":"root = Card([t1])\nt1 = Text(null)","checkpoints":[{"at":3,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":6,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":9,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":["Ca"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":12,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"missing-required","component":"Card","path":"/children","message":"missing required field \"/children\" — signature: Card(children*: array, title: string)","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":15,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["t1"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":18,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["t1"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":21,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["t1"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":24,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["T"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":27,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["Text"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":30,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["nu"],"orphaned":[],"statementCount":2,"errors":[{"code":"null-required","component":"Text","path":"/text","message":"required field \"/text\" cannot be null — signature: Text(text*: string, size: \"s\"|\"m\"|\"l\")","statementId":"t1"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":33,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[{"code":"null-required","component":"Text","path":"/text","message":"required field \"/text\" cannot be null — signature: Text(text*: string, size: \"s\"|\"m\"|\"l\")","statementId":"t1"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"required-default-fills","schema":"test","input":"root = Card([x])\nx = Tagged(1, true, \"x\", null)","checkpoints":[{"at":4,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":8,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":["C"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":12,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"missing-required","component":"Card","path":"/children","message":"missing required field \"/children\" — signature: Card(children*: array, title: string)","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":16,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["x"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":20,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["x"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":24,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["Tag"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":28,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Tagged","props":{"tone":"neutral"},"partial":true,"hasDynamicProps":false,"statementId":"x"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":32,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Tagged","props":{"count":1,"flag":null,"tone":"neutral"},"partial":true,"hasDynamicProps":false,"statementId":"x"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["t"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":36,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Tagged","props":{"count":1,"flag":true,"tone":"neutral"},"partial":true,"hasDynamicProps":false,"statementId":"x"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":40,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Tagged","props":{"count":1,"flag":true,"kind":"x","tone":"neutral"},"partial":true,"hasDynamicProps":false,"statementId":"x"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":44,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Tagged","props":{"count":1,"flag":true,"kind":"x","tone":"neutral"},"partial":true,"hasDynamicProps":false,"statementId":"x"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["nu"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":47,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Tagged","props":{"count":1,"flag":true,"kind":"x","tone":"neutral"},"partial":false,"hasDynamicProps":false,"statementId":"x"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"unknown-component","schema":"test","input":"root = Card([Mystery(\"a\"), Text(\"b\")])","checkpoints":[{"at":4,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":8,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":["C"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":12,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"missing-required","component":"Card","path":"/children","message":"missing required field \"/children\" — signature: Card(children*: array, title: string)","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":16,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["Mys"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":20,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["Mystery"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":24,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"unknown-component","component":"Mystery","path":"","message":"Unknown component \"Mystery\" — not found in catalog or builtins. Available components: Card, Text, Button, Input, Chart, Tagged","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":28,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["T"],"orphaned":[],"statementCount":1,"errors":[{"code":"unknown-component","component":"Mystery","path":"","message":"Unknown component \"Mystery\" — not found in catalog or builtins. Available components: Card, Text, Button, Input, Chart, Tagged","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":32,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"unknown-component","component":"Mystery","path":"","message":"Unknown component \"Mystery\" — not found in catalog or builtins. Available components: Card, Text, Button, Input, Chart, Tagged","statementId":"root"},{"code":"missing-required","component":"Text","path":"/text","message":"missing required field \"/text\" — signature: Text(text*: string, size: \"s\"|\"m\"|\"l\")","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":36,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"b"},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"unknown-component","component":"Mystery","path":"","message":"Unknown component \"Mystery\" — not found in catalog or builtins. Available components: Card, Text, Button, Input, Chart, Tagged","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":38,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"b"},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"unknown-component","component":"Mystery","path":"","message":"Unknown component \"Mystery\" — not found in catalog or builtins. Available components: Card, Text, Button, Input, Chart, Tagged","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"excess-args","schema":"test","input":"root = Card([Text(\"a\", \"m\", \"extra\", 4)])","checkpoints":[{"at":4,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":8,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":["C"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":12,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"missing-required","component":"Card","path":"/children","message":"missing required field \"/children\" — signature: Card(children*: array, title: string)","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":16,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["Tex"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":20,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"a"},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":24,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"a","size":""},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":28,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"a","size":"m"},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":32,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"a","size":"m"},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"excess-args","component":"Text","path":"","message":"Text takes 2 arg(s), got 3 (1 excess dropped)","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":36,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"a","size":"m"},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"excess-args","component":"Text","path":"","message":"Text takes 2 arg(s), got 3 (1 excess dropped)","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":40,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"a","size":"m"},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"excess-args","component":"Text","path":"","message":"Text takes 2 arg(s), got 4 (2 excess dropped)","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":41,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"a","size":"m"},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"excess-args","component":"Text","path":"","message":"Text takes 2 arg(s), got 4 (2 excess dropped)","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"type-mismatch-scalar","schema":"test","input":"root = Card([Text(42)], 7)","checkpoints":[{"at":3,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":6,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":9,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":["Ca"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":12,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"missing-required","component":"Card","path":"/children","message":"missing required field \"/children\" — signature: Card(children*: array, title: string)","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":15,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["Te"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":18,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"missing-required","component":"Text","path":"/text","message":"missing required field \"/text\" — signature: Text(text*: string, size: \"s\"|\"m\"|\"l\")","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":21,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"type-mismatch","component":"Text","path":"/text","message":"field \"/text\" expects string but got number","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":24,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"type-mismatch","component":"Text","path":"/text","message":"field \"/text\" expects string but got number","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":26,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"type-mismatch","component":"Text","path":"/text","message":"field \"/text\" expects string but got number","statementId":"root"},{"code":"type-mismatch","component":"Card","path":"/title","message":"field \"/title\" expects string but got number","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"enum-mismatch","schema":"test","input":"root = Card([Text(\"a\", \"xl\"), Button(\"b\", null, \"tertiary\")])","checkpoints":[{"at":6,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":12,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"missing-required","component":"Card","path":"/children","message":"missing required field \"/children\" — signature: Card(children*: array, title: string)","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":18,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"missing-required","component":"Text","path":"/text","message":"missing required field \"/text\" — signature: Text(text*: string, size: \"s\"|\"m\"|\"l\")","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":24,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"a","size":""},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":30,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"a","size":"xl"},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":36,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"a","size":"xl"},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["Button"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":42,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"a","size":"xl"},"partial":true,"hasDynamicProps":false},{"type":"element","typeName":"Button","props":{"label":"b"},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":48,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"a","size":"xl"},"partial":true,"hasDynamicProps":false},{"type":"element","typeName":"Button","props":{"label":"b","action":null},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":54,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"a","size":"xl"},"partial":true,"hasDynamicProps":false},{"type":"element","typeName":"Button","props":{"label":"b","action":null,"variant":"terti"},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":60,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"a","size":"xl"},"partial":true,"hasDynamicProps":false},{"type":"element","typeName":"Button","props":{"label":"b","action":null,"variant":"tertiary"},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":61,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"a","size":"m"},"partial":false,"hasDynamicProps":false},{"type":"element","typeName":"Button","props":{"label":"b","action":null},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"type-mismatch","component":"Text","path":"/size","message":"field \"/size\" expects one of [\"s\", \"m\", \"l\"] but got \"xl\"","statementId":"root"},{"code":"type-mismatch","component":"Button","path":"/variant","message":"field \"/variant\" expects one of [\"primary\", \"secondary\"] but got \"tertiary\"","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"enum-partial-deferred","schema":"test","input":"root = Card([Text(\"a\", \"x","checkpoints":[{"at":3,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":6,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":9,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":["Ca"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":12,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"missing-required","component":"Card","path":"/children","message":"missing required field \"/children\" — signature: Card(children*: array, title: string)","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":15,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["Te"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":18,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"missing-required","component":"Text","path":"/text","message":"missing required field \"/text\" — signature: Text(text*: string, size: \"s\"|\"m\"|\"l\")","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":21,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"a"},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":24,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"a","size":""},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":25,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"a","size":"x"},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"integer-boolean-const","schema":"test","input":"root = Card([x])\nx = Tagged(1.5, \"yes\", \"y\", \"loud\")","checkpoints":[{"at":5,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":10,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":["Car"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":15,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["x"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":20,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["x"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":25,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["Tagg"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":30,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Tagged","props":{"count":{"k":"Member","obj":{"k":"Num","v":1},"field":"?"},"tone":"neutral"},"partial":true,"hasDynamicProps":true,"statementId":"x"}]},"partial":true,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":35,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Tagged","props":{"count":1.5,"tone":"neutral"},"partial":true,"hasDynamicProps":false,"statementId":"x"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[{"code":"type-mismatch","component":"Tagged","path":"/flag","message":"field \"/flag\" expects boolean but got string","statementId":"x"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":40,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Tagged","props":{"count":1.5,"tone":"neutral"},"partial":true,"hasDynamicProps":false,"statementId":"x"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[{"code":"type-mismatch","component":"Tagged","path":"/flag","message":"field \"/flag\" expects boolean but got string","statementId":"x"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":45,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Tagged","props":{"count":1.5,"kind":"y","tone":"neutral"},"partial":true,"hasDynamicProps":false,"statementId":"x"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[{"code":"type-mismatch","component":"Tagged","path":"/flag","message":"field \"/flag\" expects boolean but got string","statementId":"x"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":50,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Tagged","props":{"count":1.5,"kind":"y","tone":"loud"},"partial":true,"hasDynamicProps":false,"statementId":"x"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[{"code":"type-mismatch","component":"Tagged","path":"/flag","message":"field \"/flag\" expects boolean but got string","statementId":"x"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":52,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Tagged","props":{"count":1.5,"tone":"loud"},"partial":false,"hasDynamicProps":false,"statementId":"x"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[{"code":"type-mismatch","component":"Tagged","path":"/flag","message":"field \"/flag\" expects boolean but got string","statementId":"x"},{"code":"type-mismatch","component":"Tagged","path":"/kind","message":"field \"/kind\" expects one of [\"x\"] but got \"y\"","statementId":"x"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"nested-object","schema":"test","input":"root = Card([c1, c2, c3])\nc1 = Chart([\"a\"], [1], {unit: \"kg\"})\nc2 = Chart([\"a\"], [1], {scale: 2})\nc3 = Chart([\"a\"], [1], {unit: 5, scale: \"big\"})","checkpoints":[{"at":13,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":26,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["c1","c2","c3"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":39,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["c2","c3"],"orphaned":[],"statementCount":2,"errors":[{"code":"missing-required","component":"Chart","path":"/values","message":"missing required field \"/values\" — signature: Chart(labels*: array, values*: array, meta: object)","statementId":"c1"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":52,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Chart","props":{"labels":["a"],"values":[1],"meta":{"un":null,"?":null}},"partial":true,"hasDynamicProps":false,"statementId":"c1"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["c2","c3"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":65,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Chart","props":{"labels":["a"],"values":[1],"meta":{"unit":"kg"}},"partial":false,"hasDynamicProps":false,"statementId":"c1"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["c2","c3"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":78,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Chart","props":{"labels":["a"],"values":[1],"meta":{"unit":"kg"}},"partial":true,"hasDynamicProps":false,"statementId":"c1"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["c3"],"orphaned":[],"statementCount":3,"errors":[{"code":"missing-required","component":"Chart","path":"/values","message":"missing required field \"/values\" — signature: Chart(labels*: array, values*: array, meta: object)","statementId":"c2"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":91,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Chart","props":{"labels":["a"],"values":[1],"meta":{"unit":"kg"}},"partial":true,"hasDynamicProps":false,"statementId":"c1"},{"type":"element","typeName":"Chart","props":{"labels":["a"],"values":[1],"meta":{"scal":null,"?":null}},"partial":true,"hasDynamicProps":false,"statementId":"c2"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["c3"],"orphaned":[],"statementCount":3,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":104,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Chart","props":{"labels":["a"],"values":[1],"meta":{"unit":"kg"}},"partial":false,"hasDynamicProps":false,"statementId":"c1"},{"type":"element","typeName":"Chart","props":{"labels":["a"],"values":[1]},"partial":false,"hasDynamicProps":false,"statementId":"c2"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["C"],"orphaned":[],"statementCount":4,"errors":[{"code":"missing-required","component":"Chart","path":"/meta/unit","message":"missing required field \"/meta/unit\"","statementId":"c2"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":117,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Chart","props":{"labels":["a"],"values":[1],"meta":{"unit":"kg"}},"partial":true,"hasDynamicProps":false,"statementId":"c1"},{"type":"element","typeName":"Chart","props":{"labels":["a"],"values":[1],"meta":{"scale":2}},"partial":true,"hasDynamicProps":false,"statementId":"c2"},{"type":"element","typeName":"Chart","props":{"labels":["a"],"values":[]},"partial":true,"hasDynamicProps":false,"statementId":"c3"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":4,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":130,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Chart","props":{"labels":["a"],"values":[1],"meta":{"unit":"kg"}},"partial":true,"hasDynamicProps":false,"statementId":"c1"},{"type":"element","typeName":"Chart","props":{"labels":["a"],"values":[1],"meta":{"scale":2}},"partial":true,"hasDynamicProps":false,"statementId":"c2"},{"type":"element","typeName":"Chart","props":{"labels":["a"],"values":[1]},"partial":true,"hasDynamicProps":false,"statementId":"c3"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":4,"errors":[{"code":"type-mismatch","component":"Chart","path":"/meta/unit","message":"field \"/meta/unit\" expects string but got number","statementId":"c3"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":143,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Chart","props":{"labels":["a"],"values":[1],"meta":{"unit":"kg"}},"partial":true,"hasDynamicProps":false,"statementId":"c1"},{"type":"element","typeName":"Chart","props":{"labels":["a"],"values":[1],"meta":{"scale":2}},"partial":true,"hasDynamicProps":false,"statementId":"c2"},{"type":"element","typeName":"Chart","props":{"labels":["a"],"values":[1]},"partial":true,"hasDynamicProps":false,"statementId":"c3"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":4,"errors":[{"code":"type-mismatch","component":"Chart","path":"/meta/unit","message":"field \"/meta/unit\" expects string but got number","statementId":"c3"},{"code":"type-mismatch","component":"Chart","path":"/meta/scale","message":"field \"/meta/scale\" expects number but got string","statementId":"c3"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":145,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Chart","props":{"labels":["a"],"values":[1],"meta":{"unit":"kg"}},"partial":false,"hasDynamicProps":false,"statementId":"c1"},{"type":"element","typeName":"Chart","props":{"labels":["a"],"values":[1]},"partial":false,"hasDynamicProps":false,"statementId":"c2"},{"type":"element","typeName":"Chart","props":{"labels":["a"],"values":[1]},"partial":false,"hasDynamicProps":false,"statementId":"c3"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":4,"errors":[{"code":"missing-required","component":"Chart","path":"/meta/unit","message":"missing required field \"/meta/unit\"","statementId":"c2"},{"code":"type-mismatch","component":"Chart","path":"/meta/unit","message":"field \"/meta/unit\" expects string but got number","statementId":"c3"},{"code":"type-mismatch","component":"Chart","path":"/meta/scale","message":"field \"/meta/scale\" expects number but got string","statementId":"c3"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"array-items-pruned","schema":"test","input":"root = Card([Chart([\"a\", 2, \"c\"], [1, \"x\", 3])])","checkpoints":[{"at":4,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":8,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":["C"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":12,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"missing-required","component":"Card","path":"/children","message":"missing required field \"/children\" — signature: Card(children*: array, title: string)","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":16,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["Cha"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":20,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"missing-required","component":"Chart","path":"/values","message":"missing required field \"/values\" — signature: Chart(labels*: array, values*: array, meta: object)","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":24,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"missing-required","component":"Chart","path":"/values","message":"missing required field \"/values\" — signature: Chart(labels*: array, values*: array, meta: object)","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":28,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"type-mismatch","component":"Chart","path":"/labels/1","message":"field \"/labels/1\" expects string but got number","statementId":"root"},{"code":"missing-required","component":"Chart","path":"/values","message":"missing required field \"/values\" — signature: Chart(labels*: array, values*: array, meta: object)","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":32,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"type-mismatch","component":"Chart","path":"/labels/1","message":"field \"/labels/1\" expects string but got number","statementId":"root"},{"code":"missing-required","component":"Chart","path":"/values","message":"missing required field \"/values\" — signature: Chart(labels*: array, values*: array, meta: object)","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":36,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Chart","props":{"labels":["a","c"],"values":[1]},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"type-mismatch","component":"Chart","path":"/labels/1","message":"field \"/labels/1\" expects string but got number","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":40,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Chart","props":{"labels":["a","c"],"values":[1]},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"type-mismatch","component":"Chart","path":"/labels/1","message":"field \"/labels/1\" expects string but got number","statementId":"root"},{"code":"type-mismatch","component":"Chart","path":"/values/1","message":"field \"/values/1\" expects number but got string","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":44,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Chart","props":{"labels":["a","c"],"values":[1,3]},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"type-mismatch","component":"Chart","path":"/labels/1","message":"field \"/labels/1\" expects string but got number","statementId":"root"},{"code":"type-mismatch","component":"Chart","path":"/values/1","message":"field \"/values/1\" expects number but got string","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":48,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Chart","props":{"labels":["a","c"],"values":[1,3]},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"type-mismatch","component":"Chart","path":"/labels/1","message":"field \"/labels/1\" expects string but got number","statementId":"root"},{"code":"type-mismatch","component":"Chart","path":"/values/1","message":"field \"/values/1\" expects number but got string","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"element-in-data-slot","schema":"test","input":"root = Card([Chart([Text(\"a\")], [1])])","checkpoints":[{"at":4,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":8,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":["C"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":12,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"missing-required","component":"Card","path":"/children","message":"missing required field \"/children\" — signature: Card(children*: array, title: string)","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":16,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["Cha"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":20,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"missing-required","component":"Chart","path":"/values","message":"missing required field \"/values\" — signature: Chart(labels*: array, values*: array, meta: object)","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":24,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["Text"],"orphaned":[],"statementCount":1,"errors":[{"code":"missing-required","component":"Chart","path":"/values","message":"missing required field \"/values\" — signature: Chart(labels*: array, values*: array, meta: object)","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":28,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"type-mismatch","component":"Chart","path":"/labels/0","message":"field \"/labels/0\" expects string but got component \"Text\"","statementId":"root"},{"code":"missing-required","component":"Chart","path":"/values","message":"missing required field \"/values\" — signature: Chart(labels*: array, values*: array, meta: object)","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":32,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"type-mismatch","component":"Chart","path":"/labels/0","message":"field \"/labels/0\" expects string but got component \"Text\"","statementId":"root"},{"code":"missing-required","component":"Chart","path":"/values","message":"missing required field \"/values\" — signature: Chart(labels*: array, values*: array, meta: object)","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":36,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Chart","props":{"labels":[],"values":[1]},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"type-mismatch","component":"Chart","path":"/labels/0","message":"field \"/labels/0\" expects string but got component \"Text\"","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":38,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Chart","props":{"labels":[],"values":[1]},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"type-mismatch","component":"Chart","path":"/labels/0","message":"field \"/labels/0\" expects string but got component \"Text\"","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"unresolved-and-orphaned","schema":"test","input":"root = Card([a, missing])\na = Text(\"x\")\nlonely = Text(\"y\")","checkpoints":[{"at":5,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":10,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":["Car"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":15,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["a"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":20,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["a","miss"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":25,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["a","missing"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":30,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["a","missing"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":35,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["missing"],"orphaned":[],"statementCount":2,"errors":[{"code":"missing-required","component":"Text","path":"/text","message":"missing required field \"/text\" — signature: Text(text*: string, size: \"s\"|\"m\"|\"l\")","statementId":"a"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":40,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"x"},"partial":false,"hasDynamicProps":false,"statementId":"a"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["missing"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":45,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"x"},"partial":false,"hasDynamicProps":false,"statementId":"a"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["missing"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":50,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"x"},"partial":false,"hasDynamicProps":false,"statementId":"a"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["missing"],"orphaned":["lonely"],"statementCount":3,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":55,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"x"},"partial":true,"hasDynamicProps":false,"statementId":"a"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["missing"],"orphaned":["lonely"],"statementCount":3,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":58,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"x"},"partial":false,"hasDynamicProps":false,"statementId":"a"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["missing"],"orphaned":["lonely"],"statementCount":3,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"cycle","schema":"test","input":"root = Card([a])\na = Card([b])\nb = Card([a])","checkpoints":[{"at":4,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":8,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":["C"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":12,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"missing-required","component":"Card","path":"/children","message":"missing required field \"/children\" — signature: Card(children*: array, title: string)","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":16,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["a"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":20,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["a"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":24,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["Car"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":28,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"a"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["b"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":32,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"a"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["b"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":36,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"a"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["C"],"orphaned":[],"statementCount":3,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":40,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"a"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":3,"errors":[{"code":"missing-required","component":"Card","path":"/children","message":"missing required field \"/children\" — signature: Card(children*: array, title: string)","statementId":"b"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":44,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"b"}]},"partial":false,"hasDynamicProps":false,"statementId":"a"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["a"],"orphaned":[],"statementCount":3,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"self-reference","schema":"test","input":"root = Card([root])","checkpoints":[{"at":2,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":4,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":6,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":8,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":["C"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":10,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":["Car"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":12,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"missing-required","component":"Card","path":"/children","message":"missing required field \"/children\" — signature: Card(children*: array, title: string)","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":14,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["r"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":16,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["roo"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":18,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["root"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":19,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["root"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"no-root-first-component","schema":"test","input":"x = 1\nmain = Card([Text(\"a\")])\nother = Card([])","checkpoints":[{"at":4,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":8,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":12,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":16,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":["main"],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":20,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"main"},"meta":{"incomplete":true,"unresolved":["T"],"orphaned":["x"],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":24,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"main"},"meta":{"incomplete":true,"unresolved":[],"orphaned":["x"],"statementCount":2,"errors":[{"code":"missing-required","component":"Text","path":"/text","message":"missing required field \"/text\" — signature: Text(text*: string, size: \"s\"|\"m\"|\"l\")","statementId":"main"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":28,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"a"},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"main"},"meta":{"incomplete":true,"unresolved":[],"orphaned":["x"],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":32,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"a"},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"main"},"meta":{"incomplete":false,"unresolved":[],"orphaned":["x"],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":36,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"a"},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"main"},"meta":{"incomplete":false,"unresolved":[],"orphaned":["x"],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":40,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"a"},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"main"},"meta":{"incomplete":false,"unresolved":[],"orphaned":["x","other"],"statementCount":3,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":44,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"a"},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"main"},"meta":{"incomplete":true,"unresolved":[],"orphaned":["x","other"],"statementCount":3,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":47,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"a"},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"main"},"meta":{"incomplete":false,"unresolved":[],"orphaned":["x","other"],"statementCount":3,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"root-name-statement","schema":"test","input":"Card = Card([Text(\"named\")])\nother = Card([])","checkpoints":[{"at":4,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":8,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":["C"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":12,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"missing-required","component":"Card","path":"/children","message":"missing required field \"/children\" — signature: Card(children*: array, title: string)","statementId":"Card"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":16,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"Card"},"meta":{"incomplete":true,"unresolved":["Tex"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":20,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"n"},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"Card"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":24,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"named"},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"Card"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":28,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"named"},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"Card"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":32,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"named"},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"Card"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":36,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"named"},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"Card"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":40,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"named"},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"Card"},"meta":{"incomplete":false,"unresolved":[],"orphaned":["other"],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":44,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"named"},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"Card"},"meta":{"incomplete":true,"unresolved":[],"orphaned":["other"],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":45,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"named"},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"Card"},"meta":{"incomplete":false,"unresolved":[],"orphaned":["other"],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"duplicate-ids","schema":"test","input":"root = Card([a])\na = Text(\"first\")\na = Text(\"second\")","checkpoints":[{"at":5,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":10,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":["Car"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":15,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["a"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":20,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["a"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":25,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["Text"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":30,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"fir"},"partial":true,"hasDynamicProps":false,"statementId":"a"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":35,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"first"},"partial":false,"hasDynamicProps":false,"statementId":"a"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":40,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["T"],"orphaned":[],"statementCount":3,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":45,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"first"},"partial":true,"hasDynamicProps":false,"statementId":"a"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":3,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":50,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"first"},"partial":true,"hasDynamicProps":false,"statementId":"a"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":3,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":53,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"second"},"partial":false,"hasDynamicProps":false,"statementId":"a"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":3,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"only-prose","schema":"test","input":"Here is your UI, it will appear below.","checkpoints":[{"at":4,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":8,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":12,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":16,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":20,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":24,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":28,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":32,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":36,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":38,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"whitespace","schema":"test","input":" \n\t \n","checkpoints":[{"at":1,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":2,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":3,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":4,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":5,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":6,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":7,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":8,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"fences-and-prose","schema":"test","input":"Sure! Here it is:\n```openui\nroot = Card([a])\na = Text(\"fenced\")\n```\nAnything else?","checkpoints":[{"at":7,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":14,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":21,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":28,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":35,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":42,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["a"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":49,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["a"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":56,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"f"},"partial":true,"hasDynamicProps":false,"statementId":"a"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":63,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"fenced"},"partial":false,"hasDynamicProps":false,"statementId":"a"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":70,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"fenced"},"partial":false,"hasDynamicProps":false,"statementId":"a"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":77,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"fenced"},"partial":false,"hasDynamicProps":false,"statementId":"a"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":82,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"fenced"},"partial":false,"hasDynamicProps":false,"statementId":"a"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"multiple-fences","schema":"test","input":"```\nroot = Card([a])\n```\nand\n```\na = Text(\"second block\")\n```","checkpoints":[{"at":6,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":12,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":["C"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":18,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["a"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":24,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["a"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":30,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["a"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":36,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["a"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":42,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[{"code":"missing-required","component":"Text","path":"/text","message":"missing required field \"/text\" — signature: Text(text*: string, size: \"s\"|\"m\"|\"l\")","statementId":"a"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":48,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"secon"},"partial":true,"hasDynamicProps":false,"statementId":"a"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":54,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"second bloc"},"partial":true,"hasDynamicProps":false,"statementId":"a"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":60,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"second block"},"partial":false,"hasDynamicProps":false,"statementId":"a"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":61,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"second block"},"partial":false,"hasDynamicProps":false,"statementId":"a"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"unclosed-fence","schema":"test","input":"```openui\nroot = Card([a])\na = Text(\"str","checkpoints":[{"at":4,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":8,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":12,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":16,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":20,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":["Car"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":24,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["a"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":28,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["a"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":32,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["T"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":36,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[{"code":"missing-required","component":"Text","path":"/text","message":"missing required field \"/text\" — signature: Text(text*: string, size: \"s\"|\"m\"|\"l\")","statementId":"a"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":40,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"str"},"partial":true,"hasDynamicProps":false,"statementId":"a"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"fence-in-string","schema":"test","input":"root = Card([a])\na = Text(\"use ``` to fence\")","checkpoints":[{"at":4,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":8,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":["C"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":12,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"missing-required","component":"Card","path":"/children","message":"missing required field \"/children\" — signature: Card(children*: array, title: string)","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":16,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["a"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":20,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["a"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":24,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["Tex"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":28,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"u"},"partial":true,"hasDynamicProps":false,"statementId":"a"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":32,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"use `"},"partial":true,"hasDynamicProps":false,"statementId":"a"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":36,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"use ``` t"},"partial":true,"hasDynamicProps":false,"statementId":"a"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":40,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"use ``` to fe"},"partial":true,"hasDynamicProps":false,"statementId":"a"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":44,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"use ``` to fence"},"partial":true,"hasDynamicProps":false,"statementId":"a"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":45,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"use ``` to fence"},"partial":false,"hasDynamicProps":false,"statementId":"a"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"comments","schema":"test","input":"root = Card([a]) // trailing\n# full line\na = Text(\"not // a comment # here\")\n// done","checkpoints":[{"at":7,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":14,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["a"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":21,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["a"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":28,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["a"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":35,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["a"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":42,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["a"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":49,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["Text"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":56,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"not /"},"partial":true,"hasDynamicProps":false,"statementId":"a"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":63,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"not // a com"},"partial":true,"hasDynamicProps":false,"statementId":"a"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":70,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"not // a comment #"},"partial":true,"hasDynamicProps":false,"statementId":"a"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":77,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"not // a comment # here"},"partial":false,"hasDynamicProps":false,"statementId":"a"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":84,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"not // a comment # here"},"partial":false,"hasDynamicProps":false,"statementId":"a"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"crlf","schema":"test","input":"root = Card([a])\r\na = Text(\"windows\")\r\n","checkpoints":[{"at":4,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":8,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":["C"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":12,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"missing-required","component":"Card","path":"/children","message":"missing required field \"/children\" — signature: Card(children*: array, title: string)","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":16,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["a"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":20,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["a"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":24,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["Te"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":28,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":""},"partial":true,"hasDynamicProps":false,"statementId":"a"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":32,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"wind"},"partial":true,"hasDynamicProps":false,"statementId":"a"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":36,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"windows"},"partial":true,"hasDynamicProps":false,"statementId":"a"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":39,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"windows"},"partial":false,"hasDynamicProps":false,"statementId":"a"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"string-escapes","schema":"test","input":"root = Card([a, b, c, d])\na = Text(\"tab\\tnew\\nquote\\\" back\\\\ uni\\u00e9 pair\\ud83d\\ude00\")\nb = Text('single \\' quote \\n esc \\q')\nc = Text(\"bad \\x escape\")\nd = Text(\"emoji 😀 and é and 漢字\")","checkpoints":[{"at":16,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["a"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":32,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["Te","b","c","d"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":48,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"tab\tnew\nqu"},"partial":true,"hasDynamicProps":false,"statementId":"a"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["b","c","d"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":64,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"tab\tnew\nquote\" back\\ uni"},"partial":true,"hasDynamicProps":false,"statementId":"a"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["b","c","d"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":80,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"tab\\tnew\\nquote\\\" back\\\\ uni\\u00e9 pair\\ud83"},"partial":true,"hasDynamicProps":false,"statementId":"a"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["b","c","d"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":96,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"tab\tnew\nquote\" back\\ unié pair😀"},"partial":false,"hasDynamicProps":false,"statementId":"a"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["Te","c","d"],"orphaned":[],"statementCount":3,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":112,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"tab\tnew\nquote\" back\\ unié pair😀"},"partial":true,"hasDynamicProps":false,"statementId":"a"},{"type":"element","typeName":"Text","props":{"text":"single ' qu"},"partial":true,"hasDynamicProps":false,"statementId":"b"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["c","d"],"orphaned":[],"statementCount":3,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":128,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"tab\tnew\nquote\" back\\ unié pair😀"},"partial":false,"hasDynamicProps":false,"statementId":"a"},{"type":"element","typeName":"Text","props":{"text":"single ' quote \n esc q"},"partial":false,"hasDynamicProps":false,"statementId":"b"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["c","d"],"orphaned":[],"statementCount":3,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":144,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"tab\tnew\nquote\" back\\ unié pair😀"},"partial":true,"hasDynamicProps":false,"statementId":"a"},{"type":"element","typeName":"Text","props":{"text":"single ' quote \n esc q"},"partial":true,"hasDynamicProps":false,"statementId":"b"},{"type":"element","typeName":"Text","props":{"text":"bad \\x"},"partial":true,"hasDynamicProps":false,"statementId":"c"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["d"],"orphaned":[],"statementCount":4,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":160,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"tab\tnew\nquote\" back\\ unié pair😀"},"partial":false,"hasDynamicProps":false,"statementId":"a"},{"type":"element","typeName":"Text","props":{"text":"single ' quote \n esc q"},"partial":false,"hasDynamicProps":false,"statementId":"b"},{"type":"element","typeName":"Text","props":{"text":"bad \\x escape"},"partial":false,"hasDynamicProps":false,"statementId":"c"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["Te"],"orphaned":[],"statementCount":5,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":176,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"tab\tnew\nquote\" back\\ unié pair😀"},"partial":true,"hasDynamicProps":false,"statementId":"a"},{"type":"element","typeName":"Text","props":{"text":"single ' quote \n esc q"},"partial":true,"hasDynamicProps":false,"statementId":"b"},{"type":"element","typeName":"Text","props":{"text":"bad \\x escape"},"partial":true,"hasDynamicProps":false,"statementId":"c"},{"type":"element","typeName":"Text","props":{"text":"emoji 😀 and"},"partial":true,"hasDynamicProps":false,"statementId":"d"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":5,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":187,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"tab\tnew\nquote\" back\\ unié pair😀"},"partial":false,"hasDynamicProps":false,"statementId":"a"},{"type":"element","typeName":"Text","props":{"text":"single ' quote \n esc q"},"partial":false,"hasDynamicProps":false,"statementId":"b"},{"type":"element","typeName":"Text","props":{"text":"bad \\x escape"},"partial":false,"hasDynamicProps":false,"statementId":"c"},{"type":"element","typeName":"Text","props":{"text":"emoji 😀 and é and 漢字"},"partial":false,"hasDynamicProps":false,"statementId":"d"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":5,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"emoji-outside-strings","schema":"test","input":"root = Card([a]) 😀\na = Text(\"ok\") ✅","checkpoints":[{"at":3,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":6,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":9,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":["Ca"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":12,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"missing-required","component":"Card","path":"/children","message":"missing required field \"/children\" — signature: Card(children*: array, title: string)","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":15,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["a"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":19,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["a"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":21,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["a"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":24,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["a"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":27,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["Tex"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":30,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":""},"partial":true,"hasDynamicProps":false,"statementId":"a"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":33,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"ok"},"partial":true,"hasDynamicProps":false,"statementId":"a"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":36,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"ok"},"partial":false,"hasDynamicProps":false,"statementId":"a"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"unterminated-string","schema":"test","input":"root = Card([a])\na = Text(\"streaming tex","checkpoints":[{"at":4,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":8,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":["C"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":12,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"missing-required","component":"Card","path":"/children","message":"missing required field \"/children\" — signature: Card(children*: array, title: string)","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":16,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["a"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":20,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["a"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":24,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["Tex"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":28,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"s"},"partial":true,"hasDynamicProps":false,"statementId":"a"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":32,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"strea"},"partial":true,"hasDynamicProps":false,"statementId":"a"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":36,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"streaming"},"partial":true,"hasDynamicProps":false,"statementId":"a"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":40,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"streaming tex"},"partial":true,"hasDynamicProps":false,"statementId":"a"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"unterminated-escape","schema":"test","input":"root = Card([a])\na = Text(\"ends with \\","checkpoints":[{"at":4,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":8,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":["C"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":12,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"missing-required","component":"Card","path":"/children","message":"missing required field \"/children\" — signature: Card(children*: array, title: string)","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":16,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["a"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":20,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["a"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":24,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["Tex"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":28,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"e"},"partial":true,"hasDynamicProps":false,"statementId":"a"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":32,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"ends"},"partial":true,"hasDynamicProps":false,"statementId":"a"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":36,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"ends with"},"partial":true,"hasDynamicProps":false,"statementId":"a"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":38,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"ends with \\"},"partial":true,"hasDynamicProps":false,"statementId":"a"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"numbers","schema":"test","input":"root = Card([Chart([\"a\",\"b\",\"c\",\"d\",\"e\",\"f\"], [-3, 1.5, 1e3, 007, -0, 2.5e-3])])\n$x = 1e\n$y = 1E+2\n$z = 3.","checkpoints":[{"at":9,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":["Ca"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":18,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["Chart"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":27,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"missing-required","component":"Chart","path":"/values","message":"missing required field \"/values\" — signature: Chart(labels*: array, values*: array, meta: object)","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":36,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"missing-required","component":"Chart","path":"/values","message":"missing required field \"/values\" — signature: Chart(labels*: array, values*: array, meta: object)","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":45,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"missing-required","component":"Chart","path":"/values","message":"missing required field \"/values\" — signature: Chart(labels*: array, values*: array, meta: object)","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":54,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Chart","props":{"labels":["a","b","c","d","e","f"],"values":[-3,1.5]},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":63,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Chart","props":{"labels":["a","b","c","d","e","f"],"values":[-3,1.5,1000,0]},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":72,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Chart","props":{"labels":["a","b","c","d","e","f"],"values":[-3,1.5,1000,7,0,{"k":"Member","obj":{"k":"Num","v":2},"field":"?"},null]},"partial":true,"hasDynamicProps":true}]},"partial":true,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":81,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Chart","props":{"labels":["a","b","c","d","e","f"],"values":[-3,1.5,1000,7,0,0.0025]},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":90,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Chart","props":{"labels":["a","b","c","d","e","f"],"values":[-3,1.5,1000,7,0,0.0025]},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{"$x":null},"queryStatements":[],"mutationStatements":[]}},{"at":99,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Chart","props":{"labels":["a","b","c","d","e","f"],"values":[-3,1.5,1000,7,0,0.0025]},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":3,"errors":[]},"stateDeclarations":{"$x":null,"$y":100},"queryStatements":[],"mutationStatements":[]}},{"at":106,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Chart","props":{"labels":["a","b","c","d","e","f"],"values":[-3,1.5,1000,7,0,0.0025]},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":4,"errors":[]},"stateDeclarations":{"$x":null,"$y":100,"$z":{"k":"Member","obj":{"k":"Num","v":3},"field":"?"}},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"operators","schema":"test","input":"$a = 1\n$b = \"2\"\nroot = Card([Text($a + $b * 2 - -1 > 3 && !$c || $a == \"1\" ? \"yes\" : \"no\")])","checkpoints":[{"at":8,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{"$a":1},"queryStatements":[],"mutationStatements":[]}},{"at":16,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{"$a":1,"$b":"2"},"queryStatements":[],"mutationStatements":[]}},{"at":24,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":["C"],"orphaned":[],"statementCount":3,"errors":[]},"stateDeclarations":{"$a":1,"$b":"2"},"queryStatements":[],"mutationStatements":[]}},{"at":32,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["Tex"],"orphaned":[],"statementCount":3,"errors":[]},"stateDeclarations":{"$a":1,"$b":"2"},"queryStatements":[],"mutationStatements":[]}},{"at":40,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":{"k":"BinOp","op":"+","left":{"k":"StateRef","n":"$a"},"right":{"k":"Null"}},"size":null},"partial":true,"hasDynamicProps":true}]},"partial":true,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":3,"errors":[]},"stateDeclarations":{"$a":1,"$b":"2"},"queryStatements":[],"mutationStatements":[]}},{"at":48,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":{"k":"BinOp","op":"-","left":{"k":"BinOp","op":"+","left":{"k":"StateRef","n":"$a"},"right":{"k":"BinOp","op":"*","left":{"k":"StateRef","n":"$b"},"right":{"k":"Num","v":2}}},"right":{"k":"Null"}},"size":null},"partial":true,"hasDynamicProps":true}]},"partial":true,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":3,"errors":[]},"stateDeclarations":{"$a":1,"$b":"2"},"queryStatements":[],"mutationStatements":[]}},{"at":56,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":{"k":"BinOp","op":"&&","left":{"k":"BinOp","op":">","left":{"k":"BinOp","op":"-","left":{"k":"BinOp","op":"+","left":{"k":"StateRef","n":"$a"},"right":{"k":"BinOp","op":"*","left":{"k":"StateRef","n":"$b"},"right":{"k":"Num","v":2}}},"right":{"k":"Num","v":-1}},"right":{"k":"Num","v":3}},"right":{"k":"Null"}},"size":null},"partial":true,"hasDynamicProps":true}]},"partial":true,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":3,"errors":[]},"stateDeclarations":{"$a":1,"$b":"2"},"queryStatements":[],"mutationStatements":[]}},{"at":64,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":{"k":"BinOp","op":"||","left":{"k":"BinOp","op":"&&","left":{"k":"BinOp","op":">","left":{"k":"BinOp","op":"-","left":{"k":"BinOp","op":"+","left":{"k":"StateRef","n":"$a"},"right":{"k":"BinOp","op":"*","left":{"k":"StateRef","n":"$b"},"right":{"k":"Num","v":2}}},"right":{"k":"Num","v":-1}},"right":{"k":"Num","v":3}},"right":{"k":"UnaryOp","op":"!","operand":{"k":"StateRef","n":"$c"}}},"right":{"k":"Null"}},"size":null},"partial":true,"hasDynamicProps":true}]},"partial":true,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":3,"errors":[]},"stateDeclarations":{"$a":1,"$b":"2","$c":null},"queryStatements":[],"mutationStatements":[]}},{"at":72,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":{"k":"BinOp","op":"||","left":{"k":"BinOp","op":"&&","left":{"k":"BinOp","op":">","left":{"k":"BinOp","op":"-","left":{"k":"BinOp","op":"+","left":{"k":"StateRef","n":"$a"},"right":{"k":"BinOp","op":"*","left":{"k":"StateRef","n":"$b"},"right":{"k":"Num","v":2}}},"right":{"k":"Num","v":-1}},"right":{"k":"Num","v":3}},"right":{"k":"UnaryOp","op":"!","operand":{"k":"StateRef","n":"$c"}}},"right":{"k":"BinOp","op":"==","left":{"k":"StateRef","n":"$a"},"right":{"k":"Str","v":""}}}},"partial":true,"hasDynamicProps":true}]},"partial":true,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":3,"errors":[]},"stateDeclarations":{"$a":1,"$b":"2","$c":null},"queryStatements":[],"mutationStatements":[]}},{"at":80,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":{"k":"Ternary","cond":{"k":"BinOp","op":"||","left":{"k":"BinOp","op":"&&","left":{"k":"BinOp","op":">","left":{"k":"BinOp","op":"-","left":{"k":"BinOp","op":"+","left":{"k":"StateRef","n":"$a"},"right":{"k":"BinOp","op":"*","left":{"k":"StateRef","n":"$b"},"right":{"k":"Num","v":2}}},"right":{"k":"Num","v":-1}},"right":{"k":"Num","v":3}},"right":{"k":"UnaryOp","op":"!","operand":{"k":"StateRef","n":"$c"}}},"right":{"k":"BinOp","op":"==","left":{"k":"StateRef","n":"$a"},"right":{"k":"Str","v":"1"}}},"then":{"k":"Str","v":"ye"},"else":{"k":"Null"}},"size":null},"partial":true,"hasDynamicProps":true}]},"partial":true,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":3,"errors":[]},"stateDeclarations":{"$a":1,"$b":"2","$c":null},"queryStatements":[],"mutationStatements":[]}},{"at":88,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":{"k":"Ternary","cond":{"k":"BinOp","op":"||","left":{"k":"BinOp","op":"&&","left":{"k":"BinOp","op":">","left":{"k":"BinOp","op":"-","left":{"k":"BinOp","op":"+","left":{"k":"StateRef","n":"$a"},"right":{"k":"BinOp","op":"*","left":{"k":"StateRef","n":"$b"},"right":{"k":"Num","v":2}}},"right":{"k":"Num","v":-1}},"right":{"k":"Num","v":3}},"right":{"k":"UnaryOp","op":"!","operand":{"k":"StateRef","n":"$c"}}},"right":{"k":"BinOp","op":"==","left":{"k":"StateRef","n":"$a"},"right":{"k":"Str","v":"1"}}},"then":{"k":"Str","v":"yes"},"else":{"k":"Str","v":"no"}}},"partial":true,"hasDynamicProps":true}]},"partial":true,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":3,"errors":[]},"stateDeclarations":{"$a":1,"$b":"2","$c":null},"queryStatements":[],"mutationStatements":[]}},{"at":92,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":{"k":"Ternary","cond":{"k":"BinOp","op":"||","left":{"k":"BinOp","op":"&&","left":{"k":"BinOp","op":">","left":{"k":"BinOp","op":"-","left":{"k":"BinOp","op":"+","left":{"k":"StateRef","n":"$a"},"right":{"k":"BinOp","op":"*","left":{"k":"StateRef","n":"$b"},"right":{"k":"Num","v":2}}},"right":{"k":"Num","v":-1}},"right":{"k":"Num","v":3}},"right":{"k":"UnaryOp","op":"!","operand":{"k":"StateRef","n":"$c"}}},"right":{"k":"BinOp","op":"==","left":{"k":"StateRef","n":"$a"},"right":{"k":"Str","v":"1"}}},"then":{"k":"Str","v":"yes"},"else":{"k":"Str","v":"no"}}},"partial":false,"hasDynamicProps":true}]},"partial":false,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":3,"errors":[]},"stateDeclarations":{"$a":1,"$b":"2","$c":null},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"multiline-ternary","schema":"test","input":"$on = true\nroot = Card([t])\nt = Text($on\n ? \"enabled\"\n : \"disabled\")","checkpoints":[{"at":6,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":12,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{"$on":true},"queryStatements":[],"mutationStatements":[]}},{"at":18,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{"$on":true},"queryStatements":[],"mutationStatements":[]}},{"at":24,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{"$on":true},"queryStatements":[],"mutationStatements":[]}},{"at":30,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["t"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{"$on":true},"queryStatements":[],"mutationStatements":[]}},{"at":36,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["Text"],"orphaned":[],"statementCount":3,"errors":[]},"stateDeclarations":{"$on":true},"queryStatements":[],"mutationStatements":[]}},{"at":42,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":{"k":"StateRef","n":"$on"}},"partial":true,"hasDynamicProps":true,"statementId":"t"}]},"partial":true,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":3,"errors":[]},"stateDeclarations":{"$on":true},"queryStatements":[],"mutationStatements":[]}},{"at":48,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":{"k":"Ternary","cond":{"k":"StateRef","n":"$on"},"then":{"k":"Str","v":"en"},"else":{"k":"Null"}}},"partial":true,"hasDynamicProps":true,"statementId":"t"}]},"partial":true,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":3,"errors":[]},"stateDeclarations":{"$on":true},"queryStatements":[],"mutationStatements":[]}},{"at":54,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":{"k":"Ternary","cond":{"k":"StateRef","n":"$on"},"then":{"k":"Str","v":"enabled"},"else":{"k":"Null"}}},"partial":true,"hasDynamicProps":true,"statementId":"t"}]},"partial":true,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":3,"errors":[]},"stateDeclarations":{"$on":true},"queryStatements":[],"mutationStatements":[]}},{"at":60,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":{"k":"Ternary","cond":{"k":"StateRef","n":"$on"},"then":{"k":"Str","v":"enabled"},"else":{"k":"Str","v":""}}},"partial":true,"hasDynamicProps":true,"statementId":"t"}]},"partial":true,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":3,"errors":[]},"stateDeclarations":{"$on":true},"queryStatements":[],"mutationStatements":[]}},{"at":66,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":{"k":"Ternary","cond":{"k":"StateRef","n":"$on"},"then":{"k":"Str","v":"enabled"},"else":{"k":"Str","v":"disabl"}}},"partial":true,"hasDynamicProps":true,"statementId":"t"}]},"partial":true,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":3,"errors":[]},"stateDeclarations":{"$on":true},"queryStatements":[],"mutationStatements":[]}},{"at":70,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":{"k":"Ternary","cond":{"k":"StateRef","n":"$on"},"then":{"k":"Str","v":"enabled"},"else":{"k":"Str","v":"disabled"}}},"partial":false,"hasDynamicProps":true,"statementId":"t"}]},"partial":false,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":3,"errors":[]},"stateDeclarations":{"$on":true},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"member-index","schema":"test","input":"$data = {rows: [{name: \"a\"}, {name: \"b\"}], \"key x\": 1}\nroot = Card([Text($data.rows[1].name), Text($data.rows.name), Text($data[\"key x\"])])","checkpoints":[{"at":12,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{"$data":{"row":null}},"queryStatements":[],"mutationStatements":[]}},{"at":24,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{"$data":{"rows":[{"name":""}]}},"queryStatements":[],"mutationStatements":[]}},{"at":36,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{"$data":{"rows":[{"name":"a"},{"name":null,"?":null}]}},"queryStatements":[],"mutationStatements":[]}},{"at":48,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{"$data":{"rows":[{"name":"a"},{"name":"b"}],"key":null}},"queryStatements":[],"mutationStatements":[]}},{"at":60,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{"$data":{"rows":[{"name":"a"},{"name":"b"}],"key x":1}},"queryStatements":[],"mutationStatements":[]}},{"at":72,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["Text"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{"$data":{"rows":[{"name":"a"},{"name":"b"}],"key x":1}},"queryStatements":[],"mutationStatements":[]}},{"at":84,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":{"k":"Index","obj":{"k":"Member","obj":{"k":"StateRef","n":"$data"},"field":"rows"},"index":{"k":"Null"}}},"partial":true,"hasDynamicProps":true}]},"partial":true,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{"$data":{"rows":[{"name":"a"},{"name":"b"}],"key x":1}},"queryStatements":[],"mutationStatements":[]}},{"at":96,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":{"k":"Member","obj":{"k":"Index","obj":{"k":"Member","obj":{"k":"StateRef","n":"$data"},"field":"rows"},"index":{"k":"Num","v":1}},"field":"name"}},"partial":true,"hasDynamicProps":true}]},"partial":true,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["Te"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{"$data":{"rows":[{"name":"a"},{"name":"b"}],"key x":1}},"queryStatements":[],"mutationStatements":[]}},{"at":108,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":{"k":"Member","obj":{"k":"Index","obj":{"k":"Member","obj":{"k":"StateRef","n":"$data"},"field":"rows"},"index":{"k":"Num","v":1}},"field":"name"}},"partial":true,"hasDynamicProps":true},{"type":"element","typeName":"Text","props":{"text":{"k":"Member","obj":{"k":"StateRef","n":"$data"},"field":"row"}},"partial":true,"hasDynamicProps":true}]},"partial":true,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{"$data":{"rows":[{"name":"a"},{"name":"b"}],"key x":1}},"queryStatements":[],"mutationStatements":[]}},{"at":120,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":{"k":"Member","obj":{"k":"Index","obj":{"k":"Member","obj":{"k":"StateRef","n":"$data"},"field":"rows"},"index":{"k":"Num","v":1}},"field":"name"}},"partial":true,"hasDynamicProps":true},{"type":"element","typeName":"Text","props":{"text":{"k":"Member","obj":{"k":"Member","obj":{"k":"StateRef","n":"$data"},"field":"rows"},"field":"name"}},"partial":true,"hasDynamicProps":true}]},"partial":true,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["Tex"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{"$data":{"rows":[{"name":"a"},{"name":"b"}],"key x":1}},"queryStatements":[],"mutationStatements":[]}},{"at":132,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":{"k":"Member","obj":{"k":"Index","obj":{"k":"Member","obj":{"k":"StateRef","n":"$data"},"field":"rows"},"index":{"k":"Num","v":1}},"field":"name"}},"partial":true,"hasDynamicProps":true},{"type":"element","typeName":"Text","props":{"text":{"k":"Member","obj":{"k":"Member","obj":{"k":"StateRef","n":"$data"},"field":"rows"},"field":"name"}},"partial":true,"hasDynamicProps":true},{"type":"element","typeName":"Text","props":{"text":{"k":"Index","obj":{"k":"StateRef","n":"$data"},"index":{"k":"Str","v":"key"}}},"partial":true,"hasDynamicProps":true}]},"partial":true,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{"$data":{"rows":[{"name":"a"},{"name":"b"}],"key x":1}},"queryStatements":[],"mutationStatements":[]}},{"at":139,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":{"k":"Member","obj":{"k":"Index","obj":{"k":"Member","obj":{"k":"StateRef","n":"$data"},"field":"rows"},"index":{"k":"Num","v":1}},"field":"name"}},"partial":false,"hasDynamicProps":true},{"type":"element","typeName":"Text","props":{"text":{"k":"Member","obj":{"k":"Member","obj":{"k":"StateRef","n":"$data"},"field":"rows"},"field":"name"}},"partial":false,"hasDynamicProps":true},{"type":"element","typeName":"Text","props":{"text":{"k":"Index","obj":{"k":"StateRef","n":"$data"},"index":{"k":"Str","v":"key x"}}},"partial":false,"hasDynamicProps":true}]},"partial":false,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{"$data":{"rows":[{"name":"a"},{"name":"b"}],"key x":1}},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"builtins","schema":"test","input":"$items = [3, 1, 2]\nroot = Card([Text(\"\" + @Count($items)), Text(\"\" + @Sum($items)), Text(@Round(2.555, 2) + \"\")])\nbare = Count($items)","checkpoints":[{"at":12,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{"$items":[3]},"queryStatements":[],"mutationStatements":[]}},{"at":24,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{"$items":[3,1,2]},"queryStatements":[],"mutationStatements":[]}},{"at":36,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["Text"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{"$items":[3,1,2]},"queryStatements":[],"mutationStatements":[]}},{"at":48,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":{"k":"BinOp","op":"+","left":{"k":"Str","v":""},"right":{"k":"Ph","n":"Count"}}},"partial":true,"hasDynamicProps":true}]},"partial":true,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["Count"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{"$items":[3,1,2]},"queryStatements":[],"mutationStatements":[]}},{"at":60,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":{"k":"BinOp","op":"+","left":{"k":"Str","v":""},"right":{"k":"Comp","name":"Count","args":[{"k":"StateRef","n":"$items"}]}}},"partial":true,"hasDynamicProps":true}]},"partial":true,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["T"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{"$items":[3,1,2]},"queryStatements":[],"mutationStatements":[]}},{"at":72,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":{"k":"BinOp","op":"+","left":{"k":"Str","v":""},"right":{"k":"Comp","name":"Count","args":[{"k":"StateRef","n":"$items"}]}}},"partial":true,"hasDynamicProps":true},{"type":"element","typeName":"Text","props":{"text":{"k":"BinOp","op":"+","left":{"k":"Str","v":""},"right":{"k":"Ph","n":"Su"}}},"partial":true,"hasDynamicProps":true}]},"partial":true,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["Su"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{"$items":[3,1,2]},"queryStatements":[],"mutationStatements":[]}},{"at":84,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":{"k":"BinOp","op":"+","left":{"k":"Str","v":""},"right":{"k":"Comp","name":"Count","args":[{"k":"StateRef","n":"$items"}]}}},"partial":true,"hasDynamicProps":true},{"type":"element","typeName":"Text","props":{"text":{"k":"BinOp","op":"+","left":{"k":"Str","v":""},"right":{"k":"Comp","name":"Sum","args":[{"k":"StateRef","n":"$items"}]}}},"partial":true,"hasDynamicProps":true}]},"partial":true,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{"$items":[3,1,2]},"queryStatements":[],"mutationStatements":[]}},{"at":96,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":{"k":"BinOp","op":"+","left":{"k":"Str","v":""},"right":{"k":"Comp","name":"Count","args":[{"k":"StateRef","n":"$items"}]}}},"partial":true,"hasDynamicProps":true},{"type":"element","typeName":"Text","props":{"text":{"k":"BinOp","op":"+","left":{"k":"Str","v":""},"right":{"k":"Comp","name":"Sum","args":[{"k":"StateRef","n":"$items"}]}}},"partial":true,"hasDynamicProps":true},{"type":"element","typeName":"Text","props":{"text":{"k":"Comp","name":"Round","args":[]}},"partial":true,"hasDynamicProps":true}]},"partial":true,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{"$items":[3,1,2]},"queryStatements":[],"mutationStatements":[]}},{"at":108,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":{"k":"BinOp","op":"+","left":{"k":"Str","v":""},"right":{"k":"Comp","name":"Count","args":[{"k":"StateRef","n":"$items"}]}}},"partial":true,"hasDynamicProps":true},{"type":"element","typeName":"Text","props":{"text":{"k":"BinOp","op":"+","left":{"k":"Str","v":""},"right":{"k":"Comp","name":"Sum","args":[{"k":"StateRef","n":"$items"}]}}},"partial":true,"hasDynamicProps":true},{"type":"element","typeName":"Text","props":{"text":{"k":"BinOp","op":"+","left":{"k":"Comp","name":"Round","args":[{"k":"Num","v":2.555},{"k":"Num","v":2}]},"right":{"k":"Null"}},"size":null},"partial":true,"hasDynamicProps":true}]},"partial":true,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{"$items":[3,1,2]},"queryStatements":[],"mutationStatements":[]}},{"at":120,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":{"k":"BinOp","op":"+","left":{"k":"Str","v":""},"right":{"k":"Comp","name":"Count","args":[{"k":"StateRef","n":"$items"}]}}},"partial":false,"hasDynamicProps":true},{"type":"element","typeName":"Text","props":{"text":{"k":"BinOp","op":"+","left":{"k":"Str","v":""},"right":{"k":"Comp","name":"Sum","args":[{"k":"StateRef","n":"$items"}]}}},"partial":false,"hasDynamicProps":true},{"type":"element","typeName":"Text","props":{"text":{"k":"BinOp","op":"+","left":{"k":"Comp","name":"Round","args":[{"k":"Num","v":2.555},{"k":"Num","v":2}]},"right":{"k":"Str","v":""}}},"partial":false,"hasDynamicProps":true}]},"partial":false,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{"$items":[3,1,2]},"queryStatements":[],"mutationStatements":[]}},{"at":132,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":{"k":"BinOp","op":"+","left":{"k":"Str","v":""},"right":{"k":"Comp","name":"Count","args":[{"k":"StateRef","n":"$items"}]}}},"partial":true,"hasDynamicProps":true},{"type":"element","typeName":"Text","props":{"text":{"k":"BinOp","op":"+","left":{"k":"Str","v":""},"right":{"k":"Comp","name":"Sum","args":[{"k":"StateRef","n":"$items"}]}}},"partial":true,"hasDynamicProps":true},{"type":"element","typeName":"Text","props":{"text":{"k":"BinOp","op":"+","left":{"k":"Comp","name":"Round","args":[{"k":"Num","v":2.555},{"k":"Num","v":2}]},"right":{"k":"Str","v":""}}},"partial":true,"hasDynamicProps":true}]},"partial":true,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":["bare"],"statementCount":3,"errors":[]},"stateDeclarations":{"$items":[3,1,2]},"queryStatements":[],"mutationStatements":[]}},{"at":134,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":{"k":"BinOp","op":"+","left":{"k":"Str","v":""},"right":{"k":"Comp","name":"Count","args":[{"k":"StateRef","n":"$items"}]}}},"partial":false,"hasDynamicProps":true},{"type":"element","typeName":"Text","props":{"text":{"k":"BinOp","op":"+","left":{"k":"Str","v":""},"right":{"k":"Comp","name":"Sum","args":[{"k":"StateRef","n":"$items"}]}}},"partial":false,"hasDynamicProps":true},{"type":"element","typeName":"Text","props":{"text":{"k":"BinOp","op":"+","left":{"k":"Comp","name":"Round","args":[{"k":"Num","v":2.555},{"k":"Num","v":2}]},"right":{"k":"Str","v":""}}},"partial":false,"hasDynamicProps":true}]},"partial":false,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":["bare"],"statementCount":3,"errors":[]},"stateDeclarations":{"$items":[3,1,2]},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"actions","schema":"test","input":"$n = 0\nroot = Card([b1, b2, b3])\nb1 = Button(\"Inc\", Action([@Set($n, $n + 1), @ToAssistant(\"Bumped\", \"ctx\")]))\nb2 = Button(\"Reset\", Action([@Reset($n)]))\nb3 = Button(\"Docs\", Action([@OpenUrl(\"https://openui.com\")]))","checkpoints":[{"at":18,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":["Card"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{"$n":0},"queryStatements":[],"mutationStatements":[]}},{"at":36,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["b1","b2","b3"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{"$n":0},"queryStatements":[],"mutationStatements":[]}},{"at":54,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Button","props":{"label":"Inc","action":null},"partial":true,"hasDynamicProps":false,"statementId":"b1"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["Ac","b2","b3"],"orphaned":[],"statementCount":3,"errors":[]},"stateDeclarations":{"$n":0},"queryStatements":[],"mutationStatements":[]}},{"at":72,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Button","props":{"label":"Inc","action":{"k":"Comp","name":"Action","args":[{"k":"Arr","els":[{"k":"Comp","name":"Set","args":[{"k":"StateRef","n":"$n"},{"k":"StateRef","n":"$n"}]}]}]}},"partial":true,"hasDynamicProps":true,"statementId":"b1"}]},"partial":true,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["b2","b3"],"orphaned":[],"statementCount":3,"errors":[]},"stateDeclarations":{"$n":0},"queryStatements":[],"mutationStatements":[]}},{"at":90,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Button","props":{"label":"Inc","action":{"k":"Comp","name":"Action","args":[{"k":"Arr","els":[{"k":"Comp","name":"Set","args":[{"k":"StateRef","n":"$n"},{"k":"BinOp","op":"+","left":{"k":"StateRef","n":"$n"},"right":{"k":"Num","v":1}}]},{"k":"Ph","n":"ToAssistant"}]}]}},"partial":true,"hasDynamicProps":true,"statementId":"b1"}]},"partial":true,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["ToAssistant","b2","b3"],"orphaned":[],"statementCount":3,"errors":[]},"stateDeclarations":{"$n":0},"queryStatements":[],"mutationStatements":[]}},{"at":108,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Button","props":{"label":"Inc","action":{"k":"Comp","name":"Action","args":[{"k":"Arr","els":[{"k":"Comp","name":"Set","args":[{"k":"StateRef","n":"$n"},{"k":"BinOp","op":"+","left":{"k":"StateRef","n":"$n"},"right":{"k":"Num","v":1}}]},{"k":"Comp","name":"ToAssistant","args":[{"k":"Str","v":"Bumped"},{"k":"Str","v":"ctx"}]}]}]}},"partial":true,"hasDynamicProps":true,"statementId":"b1"}]},"partial":true,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["b2","b3"],"orphaned":[],"statementCount":3,"errors":[]},"stateDeclarations":{"$n":0},"queryStatements":[],"mutationStatements":[]}},{"at":126,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Button","props":{"label":"Inc","action":{"k":"Comp","name":"Action","args":[{"k":"Arr","els":[{"k":"Comp","name":"Set","args":[{"k":"StateRef","n":"$n"},{"k":"BinOp","op":"+","left":{"k":"StateRef","n":"$n"},"right":{"k":"Num","v":1}}]},{"k":"Comp","name":"ToAssistant","args":[{"k":"Str","v":"Bumped"},{"k":"Str","v":"ctx"}]}]}]}},"partial":true,"hasDynamicProps":true,"statementId":"b1"},{"type":"element","typeName":"Button","props":{"label":"Re"},"partial":true,"hasDynamicProps":false,"statementId":"b2"}]},"partial":true,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["b3"],"orphaned":[],"statementCount":4,"errors":[]},"stateDeclarations":{"$n":0},"queryStatements":[],"mutationStatements":[]}},{"at":144,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Button","props":{"label":"Inc","action":{"k":"Comp","name":"Action","args":[{"k":"Arr","els":[{"k":"Comp","name":"Set","args":[{"k":"StateRef","n":"$n"},{"k":"BinOp","op":"+","left":{"k":"StateRef","n":"$n"},"right":{"k":"Num","v":1}}]},{"k":"Comp","name":"ToAssistant","args":[{"k":"Str","v":"Bumped"},{"k":"Str","v":"ctx"}]}]}]}},"partial":true,"hasDynamicProps":true,"statementId":"b1"},{"type":"element","typeName":"Button","props":{"label":"Reset","action":{"k":"Comp","name":"Action","args":[{"k":"Arr","els":[{"k":"Ph","n":"Res"}]}]}},"partial":true,"hasDynamicProps":true,"statementId":"b2"}]},"partial":true,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["Res","b3"],"orphaned":[],"statementCount":4,"errors":[]},"stateDeclarations":{"$n":0},"queryStatements":[],"mutationStatements":[]}},{"at":162,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Button","props":{"label":"Inc","action":{"k":"Comp","name":"Action","args":[{"k":"Arr","els":[{"k":"Comp","name":"Set","args":[{"k":"StateRef","n":"$n"},{"k":"BinOp","op":"+","left":{"k":"StateRef","n":"$n"},"right":{"k":"Num","v":1}}]},{"k":"Comp","name":"ToAssistant","args":[{"k":"Str","v":"Bumped"},{"k":"Str","v":"ctx"}]}]}]}},"partial":false,"hasDynamicProps":true,"statementId":"b1"},{"type":"element","typeName":"Button","props":{"label":"Reset","action":{"k":"Comp","name":"Action","args":[{"k":"Arr","els":[{"k":"Comp","name":"Reset","args":[{"k":"StateRef","n":"$n"}]}]}]}},"partial":false,"hasDynamicProps":true,"statementId":"b2"}]},"partial":false,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["But"],"orphaned":[],"statementCount":5,"errors":[]},"stateDeclarations":{"$n":0},"queryStatements":[],"mutationStatements":[]}},{"at":180,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Button","props":{"label":"Inc","action":{"k":"Comp","name":"Action","args":[{"k":"Arr","els":[{"k":"Comp","name":"Set","args":[{"k":"StateRef","n":"$n"},{"k":"BinOp","op":"+","left":{"k":"StateRef","n":"$n"},"right":{"k":"Num","v":1}}]},{"k":"Comp","name":"ToAssistant","args":[{"k":"Str","v":"Bumped"},{"k":"Str","v":"ctx"}]}]}]}},"partial":true,"hasDynamicProps":true,"statementId":"b1"},{"type":"element","typeName":"Button","props":{"label":"Reset","action":{"k":"Comp","name":"Action","args":[{"k":"Arr","els":[{"k":"Comp","name":"Reset","args":[{"k":"StateRef","n":"$n"}]}]}]}},"partial":true,"hasDynamicProps":true,"statementId":"b2"},{"type":"element","typeName":"Button","props":{"label":"Docs","action":null},"partial":true,"hasDynamicProps":false,"statementId":"b3"}]},"partial":true,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["Action"],"orphaned":[],"statementCount":5,"errors":[]},"stateDeclarations":{"$n":0},"queryStatements":[],"mutationStatements":[]}},{"at":198,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Button","props":{"label":"Inc","action":{"k":"Comp","name":"Action","args":[{"k":"Arr","els":[{"k":"Comp","name":"Set","args":[{"k":"StateRef","n":"$n"},{"k":"BinOp","op":"+","left":{"k":"StateRef","n":"$n"},"right":{"k":"Num","v":1}}]},{"k":"Comp","name":"ToAssistant","args":[{"k":"Str","v":"Bumped"},{"k":"Str","v":"ctx"}]}]}]}},"partial":true,"hasDynamicProps":true,"statementId":"b1"},{"type":"element","typeName":"Button","props":{"label":"Reset","action":{"k":"Comp","name":"Action","args":[{"k":"Arr","els":[{"k":"Comp","name":"Reset","args":[{"k":"StateRef","n":"$n"}]}]}]}},"partial":true,"hasDynamicProps":true,"statementId":"b2"},{"type":"element","typeName":"Button","props":{"label":"Docs","action":{"k":"Comp","name":"Action","args":[{"k":"Arr","els":[{"k":"Comp","name":"OpenUrl","args":[{"k":"Str","v":"https:"}]}]}]}},"partial":true,"hasDynamicProps":true,"statementId":"b3"}]},"partial":true,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":5,"errors":[]},"stateDeclarations":{"$n":0},"queryStatements":[],"mutationStatements":[]}},{"at":215,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Button","props":{"label":"Inc","action":{"k":"Comp","name":"Action","args":[{"k":"Arr","els":[{"k":"Comp","name":"Set","args":[{"k":"StateRef","n":"$n"},{"k":"BinOp","op":"+","left":{"k":"StateRef","n":"$n"},"right":{"k":"Num","v":1}}]},{"k":"Comp","name":"ToAssistant","args":[{"k":"Str","v":"Bumped"},{"k":"Str","v":"ctx"}]}]}]}},"partial":false,"hasDynamicProps":true,"statementId":"b1"},{"type":"element","typeName":"Button","props":{"label":"Reset","action":{"k":"Comp","name":"Action","args":[{"k":"Arr","els":[{"k":"Comp","name":"Reset","args":[{"k":"StateRef","n":"$n"}]}]}]}},"partial":false,"hasDynamicProps":true,"statementId":"b2"},{"type":"element","typeName":"Button","props":{"label":"Docs","action":{"k":"Comp","name":"Action","args":[{"k":"Arr","els":[{"k":"Comp","name":"OpenUrl","args":[{"k":"Str","v":"https://openui.com"}]}]}]}},"partial":false,"hasDynamicProps":true,"statementId":"b3"}]},"partial":false,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":5,"errors":[]},"stateDeclarations":{"$n":0},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"each","schema":"test","input":"$todos = [{title: \"a\", done: false}, {title: \"b\", done: true}]\nroot = Card(@Each($todos, todo, Text(todo.title)))","checkpoints":[{"at":10,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{"$todos":[]},"queryStatements":[],"mutationStatements":[]}},{"at":20,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{"$todos":[{"title":"a"}]},"queryStatements":[],"mutationStatements":[]}},{"at":30,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":["f","f"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{"$todos":[{"title":"a","done":null}]},"queryStatements":[],"mutationStatements":[]}},{"at":40,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{"$todos":[{"title":"a","done":false},{"ti":null,"?":null}]},"queryStatements":[],"mutationStatements":[]}},{"at":50,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{"$todos":[{"title":"a","done":false},{"title":"b"}]},"queryStatements":[],"mutationStatements":[]}},{"at":60,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{"$todos":[{"title":"a","done":false},{"title":"b","done":true}]},"queryStatements":[],"mutationStatements":[]}},{"at":70,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{"$todos":[{"title":"a","done":false},{"title":"b","done":true}]},"queryStatements":[],"mutationStatements":[]}},{"at":80,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":["Each"],"orphaned":[],"statementCount":2,"errors":[{"code":"null-required","component":"Card","path":"/children","message":"required field \"/children\" cannot be null — signature: Card(children*: array, title: string)","statementId":"root"}]},"stateDeclarations":{"$todos":[{"title":"a","done":false},{"title":"b","done":true}]},"queryStatements":[],"mutationStatements":[]}},{"at":90,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":{"k":"Comp","name":"Each","args":[{"k":"StateRef","n":"$todos"},{"k":"Ph","n":"t"}]}},"partial":true,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["t"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{"$todos":[{"title":"a","done":false},{"title":"b","done":true}]},"queryStatements":[],"mutationStatements":[]}},{"at":100,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":{"k":"Comp","name":"Each","args":[{"k":"StateRef","n":"$todos"},{"k":"Ref","n":"todo"},{"k":"Comp","name":"Text","args":[],"mappedProps":{}}]}},"partial":true,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{"$todos":[{"title":"a","done":false},{"title":"b","done":true}]},"queryStatements":[],"mutationStatements":[]}},{"at":110,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":{"k":"Comp","name":"Each","args":[{"k":"StateRef","n":"$todos"},{"k":"Ref","n":"todo"},{"k":"Comp","name":"Text","args":[{"k":"Member","obj":{"k":"Ref","n":"todo"},"field":"title"}],"mappedProps":{"text":{"k":"Member","obj":{"k":"Ref","n":"todo"},"field":"title"}}}]}},"partial":true,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{"$todos":[{"title":"a","done":false},{"title":"b","done":true}]},"queryStatements":[],"mutationStatements":[]}},{"at":113,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":{"k":"Comp","name":"Each","args":[{"k":"StateRef","n":"$todos"},{"k":"Ref","n":"todo"},{"k":"Comp","name":"Text","args":[{"k":"Member","obj":{"k":"Ref","n":"todo"},"field":"title"}],"mappedProps":{"text":{"k":"Member","obj":{"k":"Ref","n":"todo"},"field":"title"}}}]}},"partial":false,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{"$todos":[{"title":"a","done":false},{"title":"b","done":true}]},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"query-mutation","schema":"test","input":"$q = \"x\"\nusers = Query(\"list_users\", {search: $q, page: $page}, {rows: []}, 30)\nsave = Mutation(\"save_user\", {name: $q})\nroot = Card([Text(users.rows.length + \" users\"), Button(\"Save\", Action([@Run(save), @Run(users)]))])","checkpoints":[{"at":19,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":["users"],"statementCount":2,"errors":[]},"stateDeclarations":{"$q":"x"},"queryStatements":[],"mutationStatements":[]}},{"at":38,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{"$q":"x"},"queryStatements":[{"statementId":"users","toolAST":{"k":"Str","v":"list_users"},"argsAST":{"k":"Obj","entries":[]},"defaultsAST":null,"refreshAST":null,"complete":true}],"mutationStatements":[]}},{"at":57,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{"$q":"x"},"queryStatements":[{"statementId":"users","toolAST":{"k":"Str","v":"list_users"},"argsAST":{"k":"Obj","entries":[["search",{"k":"StateRef","n":"$q"}],["page",{"k":"Null"}],["?",{"k":"Null"}]]},"defaultsAST":null,"refreshAST":null,"deps":["$q"],"complete":true}],"mutationStatements":[]}},{"at":76,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{"$q":"x","$page":null},"queryStatements":[{"statementId":"users","toolAST":{"k":"Str","v":"list_users"},"argsAST":{"k":"Obj","entries":[["search",{"k":"StateRef","n":"$q"}],["page",{"k":"StateRef","n":"$page"}]]},"defaultsAST":{"k":"Obj","entries":[["rows",{"k":"Arr","els":[]}]]},"refreshAST":null,"deps":["$q","$page"],"complete":true}],"mutationStatements":[]}},{"at":95,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":["save"],"statementCount":3,"errors":[]},"stateDeclarations":{"$q":"x","$page":null},"queryStatements":[{"statementId":"users","toolAST":{"k":"Str","v":"list_users"},"argsAST":{"k":"Obj","entries":[["search",{"k":"StateRef","n":"$q"}],["page",{"k":"StateRef","n":"$page"}]]},"defaultsAST":{"k":"Obj","entries":[["rows",{"k":"Arr","els":[]}]]},"refreshAST":{"k":"Num","v":30},"deps":["$q","$page"],"complete":true}],"mutationStatements":[]}},{"at":114,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":3,"errors":[]},"stateDeclarations":{"$q":"x","$page":null},"queryStatements":[{"statementId":"users","toolAST":{"k":"Str","v":"list_users"},"argsAST":{"k":"Obj","entries":[["search",{"k":"StateRef","n":"$q"}],["page",{"k":"StateRef","n":"$page"}]]},"defaultsAST":{"k":"Obj","entries":[["rows",{"k":"Arr","els":[]}]]},"refreshAST":{"k":"Num","v":30},"deps":["$q","$page"],"complete":true}],"mutationStatements":[{"statementId":"save","toolAST":{"k":"Str","v":"save_user"},"argsAST":{"k":"Obj","entries":[["name",{"k":"Null"}],["?",{"k":"Null"}]]}}]}},{"at":133,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":4,"errors":[{"code":"missing-required","component":"Card","path":"/children","message":"missing required field \"/children\" — signature: Card(children*: array, title: string)","statementId":"root"}]},"stateDeclarations":{"$q":"x","$page":null},"queryStatements":[{"statementId":"users","toolAST":{"k":"Str","v":"list_users"},"argsAST":{"k":"Obj","entries":[["search",{"k":"StateRef","n":"$q"}],["page",{"k":"StateRef","n":"$page"}]]},"defaultsAST":{"k":"Obj","entries":[["rows",{"k":"Arr","els":[]}]]},"refreshAST":{"k":"Num","v":30},"deps":["$q","$page"],"complete":true}],"mutationStatements":[{"statementId":"save","toolAST":{"k":"Str","v":"save_user"},"argsAST":{"k":"Obj","entries":[["name",{"k":"StateRef","n":"$q"}]]}}]}},{"at":152,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":{"k":"Member","obj":{"k":"Member","obj":{"k":"RuntimeRef","n":"users","refType":"query"},"field":"rows"},"field":"le"}},"partial":true,"hasDynamicProps":true}]},"partial":true,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":4,"errors":[]},"stateDeclarations":{"$q":"x","$page":null},"queryStatements":[{"statementId":"users","toolAST":{"k":"Str","v":"list_users"},"argsAST":{"k":"Obj","entries":[["search",{"k":"StateRef","n":"$q"}],["page",{"k":"StateRef","n":"$page"}]]},"defaultsAST":{"k":"Obj","entries":[["rows",{"k":"Arr","els":[]}]]},"refreshAST":{"k":"Num","v":30},"deps":["$q","$page"],"complete":true}],"mutationStatements":[{"statementId":"save","toolAST":{"k":"Str","v":"save_user"},"argsAST":{"k":"Obj","entries":[["name",{"k":"StateRef","n":"$q"}]]}}]}},{"at":171,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":{"k":"BinOp","op":"+","left":{"k":"Member","obj":{"k":"Member","obj":{"k":"RuntimeRef","n":"users","refType":"query"},"field":"rows"},"field":"length"},"right":{"k":"Str","v":" users"}}},"partial":true,"hasDynamicProps":true}]},"partial":true,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["B"],"orphaned":[],"statementCount":4,"errors":[]},"stateDeclarations":{"$q":"x","$page":null},"queryStatements":[{"statementId":"users","toolAST":{"k":"Str","v":"list_users"},"argsAST":{"k":"Obj","entries":[["search",{"k":"StateRef","n":"$q"}],["page",{"k":"StateRef","n":"$page"}]]},"defaultsAST":{"k":"Obj","entries":[["rows",{"k":"Arr","els":[]}]]},"refreshAST":{"k":"Num","v":30},"deps":["$q","$page"],"complete":true}],"mutationStatements":[{"statementId":"save","toolAST":{"k":"Str","v":"save_user"},"argsAST":{"k":"Obj","entries":[["name",{"k":"StateRef","n":"$q"}]]}}]}},{"at":190,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":{"k":"BinOp","op":"+","left":{"k":"Member","obj":{"k":"Member","obj":{"k":"RuntimeRef","n":"users","refType":"query"},"field":"rows"},"field":"length"},"right":{"k":"Str","v":" users"}}},"partial":true,"hasDynamicProps":true},{"type":"element","typeName":"Button","props":{"label":"Save","action":null},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["Actio"],"orphaned":[],"statementCount":4,"errors":[]},"stateDeclarations":{"$q":"x","$page":null},"queryStatements":[{"statementId":"users","toolAST":{"k":"Str","v":"list_users"},"argsAST":{"k":"Obj","entries":[["search",{"k":"StateRef","n":"$q"}],["page",{"k":"StateRef","n":"$page"}]]},"defaultsAST":{"k":"Obj","entries":[["rows",{"k":"Arr","els":[]}]]},"refreshAST":{"k":"Num","v":30},"deps":["$q","$page"],"complete":true}],"mutationStatements":[{"statementId":"save","toolAST":{"k":"Str","v":"save_user"},"argsAST":{"k":"Obj","entries":[["name",{"k":"StateRef","n":"$q"}]]}}]}},{"at":209,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":{"k":"BinOp","op":"+","left":{"k":"Member","obj":{"k":"Member","obj":{"k":"RuntimeRef","n":"users","refType":"query"},"field":"rows"},"field":"length"},"right":{"k":"Str","v":" users"}}},"partial":true,"hasDynamicProps":true},{"type":"element","typeName":"Button","props":{"label":"Save","action":{"k":"Comp","name":"Action","args":[{"k":"Arr","els":[{"k":"Comp","name":"Run","args":[{"k":"RuntimeRef","n":"save","refType":"mutation"}]},{"k":"Ph","n":"Run"}]}]}},"partial":true,"hasDynamicProps":true}]},"partial":true,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["Run"],"orphaned":[],"statementCount":4,"errors":[]},"stateDeclarations":{"$q":"x","$page":null},"queryStatements":[{"statementId":"users","toolAST":{"k":"Str","v":"list_users"},"argsAST":{"k":"Obj","entries":[["search",{"k":"StateRef","n":"$q"}],["page",{"k":"StateRef","n":"$page"}]]},"defaultsAST":{"k":"Obj","entries":[["rows",{"k":"Arr","els":[]}]]},"refreshAST":{"k":"Num","v":30},"deps":["$q","$page"],"complete":true}],"mutationStatements":[{"statementId":"save","toolAST":{"k":"Str","v":"save_user"},"argsAST":{"k":"Obj","entries":[["name",{"k":"StateRef","n":"$q"}]]}}]}},{"at":221,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":{"k":"BinOp","op":"+","left":{"k":"Member","obj":{"k":"Member","obj":{"k":"RuntimeRef","n":"users","refType":"query"},"field":"rows"},"field":"length"},"right":{"k":"Str","v":" users"}}},"partial":false,"hasDynamicProps":true},{"type":"element","typeName":"Button","props":{"label":"Save","action":{"k":"Comp","name":"Action","args":[{"k":"Arr","els":[{"k":"Comp","name":"Run","args":[{"k":"RuntimeRef","n":"save","refType":"mutation"}]},{"k":"Comp","name":"Run","args":[{"k":"RuntimeRef","n":"users","refType":"query"}]}]}]}},"partial":false,"hasDynamicProps":true}]},"partial":false,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":4,"errors":[]},"stateDeclarations":{"$q":"x","$page":null},"queryStatements":[{"statementId":"users","toolAST":{"k":"Str","v":"list_users"},"argsAST":{"k":"Obj","entries":[["search",{"k":"StateRef","n":"$q"}],["page",{"k":"StateRef","n":"$page"}]]},"defaultsAST":{"k":"Obj","entries":[["rows",{"k":"Arr","els":[]}]]},"refreshAST":{"k":"Num","v":30},"deps":["$q","$page"],"complete":true}],"mutationStatements":[{"statementId":"save","toolAST":{"k":"Str","v":"save_user"},"argsAST":{"k":"Obj","entries":[["name",{"k":"StateRef","n":"$q"}]]}}]}}]}, +{"name":"state-query","schema":"test","input":"$data = Query(\"get\", {})\nroot = Card([Text(\"x\")])","checkpoints":[{"at":5,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":10,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":["Qu","Qu"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{"$data":null},"queryStatements":[],"mutationStatements":[]}},{"at":15,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"inline-reserved","component":"Query","path":"","message":"Query() must be declared as a top-level statement, not used inline as a value","statementId":"$data"}]},"stateDeclarations":{},"queryStatements":[{"statementId":"$data","toolAST":{"k":"Str","v":""},"argsAST":null,"defaultsAST":null,"refreshAST":null,"complete":true}],"mutationStatements":[]}},{"at":20,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"inline-reserved","component":"Query","path":"","message":"Query() must be declared as a top-level statement, not used inline as a value","statementId":"$data"}]},"stateDeclarations":{},"queryStatements":[{"statementId":"$data","toolAST":{"k":"Str","v":"get"},"argsAST":null,"defaultsAST":null,"refreshAST":null,"complete":true}],"mutationStatements":[]}},{"at":25,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"inline-reserved","component":"Query","path":"","message":"Query() must be declared as a top-level statement, not used inline as a value","statementId":"$data"}]},"stateDeclarations":{},"queryStatements":[{"statementId":"$data","toolAST":{"k":"Str","v":"get"},"argsAST":{"k":"Obj","entries":[]},"defaultsAST":null,"refreshAST":null,"complete":true}],"mutationStatements":[]}},{"at":30,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"inline-reserved","component":"Query","path":"","message":"Query() must be declared as a top-level statement, not used inline as a value","statementId":"$data"}]},"stateDeclarations":{},"queryStatements":[{"statementId":"$data","toolAST":{"k":"Str","v":"get"},"argsAST":{"k":"Obj","entries":[]},"defaultsAST":null,"refreshAST":null,"complete":true}],"mutationStatements":[]}},{"at":35,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":["Car"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[{"statementId":"$data","toolAST":{"k":"Str","v":"get"},"argsAST":{"k":"Obj","entries":[]},"defaultsAST":null,"refreshAST":null,"complete":true}],"mutationStatements":[]}},{"at":40,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["Te"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[{"statementId":"$data","toolAST":{"k":"Str","v":"get"},"argsAST":{"k":"Obj","entries":[]},"defaultsAST":null,"refreshAST":null,"complete":true}],"mutationStatements":[]}},{"at":45,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"x"},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[{"statementId":"$data","toolAST":{"k":"Str","v":"get"},"argsAST":{"k":"Obj","entries":[]},"defaultsAST":null,"refreshAST":null,"complete":true}],"mutationStatements":[]}},{"at":49,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"x"},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[{"statementId":"$data","toolAST":{"k":"Str","v":"get"},"argsAST":{"k":"Obj","entries":[]},"defaultsAST":null,"refreshAST":null,"complete":true}],"mutationStatements":[]}}]}, +{"name":"inline-query","schema":"test","input":"root = Card([Text(Query(\"get\", {}))])","checkpoints":[{"at":4,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":8,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":["C"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":12,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"missing-required","component":"Card","path":"/children","message":"missing required field \"/children\" — signature: Card(children*: array, title: string)","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":16,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["Tex"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":20,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["Qu"],"orphaned":[],"statementCount":1,"errors":[{"code":"null-required","component":"Text","path":"/text","message":"required field \"/text\" cannot be null — signature: Text(text*: string, size: \"s\"|\"m\"|\"l\")","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":24,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"inline-reserved","component":"Query","path":"","message":"Query() must be declared as a top-level statement, not used inline as a value","statementId":"root"},{"code":"null-required","component":"Text","path":"/text","message":"required field \"/text\" cannot be null — signature: Text(text*: string, size: \"s\"|\"m\"|\"l\")","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":28,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"inline-reserved","component":"Query","path":"","message":"Query() must be declared as a top-level statement, not used inline as a value","statementId":"root"},{"code":"null-required","component":"Text","path":"/text","message":"required field \"/text\" cannot be null — signature: Text(text*: string, size: \"s\"|\"m\"|\"l\")","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":32,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"inline-reserved","component":"Query","path":"","message":"Query() must be declared as a top-level statement, not used inline as a value","statementId":"root"},{"code":"null-required","component":"Text","path":"/text","message":"required field \"/text\" cannot be null — signature: Text(text*: string, size: \"s\"|\"m\"|\"l\")","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":36,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"inline-reserved","component":"Query","path":"","message":"Query() must be declared as a top-level statement, not used inline as a value","statementId":"root"},{"code":"null-required","component":"Text","path":"/text","message":"required field \"/text\" cannot be null — signature: Text(text*: string, size: \"s\"|\"m\"|\"l\")","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":37,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"inline-reserved","component":"Query","path":"","message":"Query() must be declared as a top-level statement, not used inline as a value","statementId":"root"},{"code":"null-required","component":"Text","path":"/text","message":"required field \"/text\" cannot be null — signature: Text(text*: string, size: \"s\"|\"m\"|\"l\")","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"assignment","schema":"test","input":"$v = \"\"\nroot = Card([Input(\"name\", $v = $value)])","checkpoints":[{"at":5,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":10,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{"$v":""},"queryStatements":[],"mutationStatements":[]}},{"at":15,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{"$v":""},"queryStatements":[],"mutationStatements":[]}},{"at":20,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[{"code":"missing-required","component":"Card","path":"/children","message":"missing required field \"/children\" — signature: Card(children*: array, title: string)","statementId":"root"}]},"stateDeclarations":{"$v":""},"queryStatements":[],"mutationStatements":[]}},{"at":25,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["Inpu"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{"$v":""},"queryStatements":[],"mutationStatements":[]}},{"at":30,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Input","props":{"name":"na"},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{"$v":""},"queryStatements":[],"mutationStatements":[]}},{"at":35,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Input","props":{"name":"name"},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{"$v":""},"queryStatements":[],"mutationStatements":[]}},{"at":40,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Input","props":{"name":"name","value":{"k":"Assign","target":"$v","value":{"k":"Null"}}},"partial":true,"hasDynamicProps":true}]},"partial":true,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[{"code":"excess-args","component":"Input","path":"","message":"Input takes 2 arg(s), got 3 (1 excess dropped)","statementId":"root"}]},"stateDeclarations":{"$v":""},"queryStatements":[],"mutationStatements":[]}},{"at":45,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Input","props":{"name":"name","value":{"k":"Assign","target":"$v","value":{"k":"StateRef","n":"$valu"}}},"partial":true,"hasDynamicProps":true}]},"partial":true,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{"$v":"","$valu":null},"queryStatements":[],"mutationStatements":[]}},{"at":49,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Input","props":{"name":"name","value":{"k":"Assign","target":"$v","value":{"k":"StateRef","n":"$value"}}},"partial":false,"hasDynamicProps":true}]},"partial":false,"hasDynamicProps":true,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{"$v":"","$value":null},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"object-key-order","schema":"test","input":"$o = {b: 1, 2: \"two\", a: 3, 1: \"one\", \"10\": 10}\nroot = Card([Text(\"x\")])","checkpoints":[{"at":6,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{"$o":{}},"queryStatements":[],"mutationStatements":[]}},{"at":12,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{"$o":{"b":1}},"queryStatements":[],"mutationStatements":[]}},{"at":18,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{"$o":{"2":"tw","b":1}},"queryStatements":[],"mutationStatements":[]}},{"at":24,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{"$o":{"2":"two","b":1,"a":null}},"queryStatements":[],"mutationStatements":[]}},{"at":30,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{"$o":{"1":null,"2":"two","b":1,"a":3}},"queryStatements":[],"mutationStatements":[]}},{"at":36,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{"$o":{"1":"one","2":"two","b":1,"a":3}},"queryStatements":[],"mutationStatements":[]}},{"at":42,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{"$o":{"1":"one","2":"two","10":null,"b":1,"a":3}},"queryStatements":[],"mutationStatements":[]}},{"at":48,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{"$o":{"1":"one","2":"two","10":10,"b":1,"a":3}},"queryStatements":[],"mutationStatements":[]}},{"at":54,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{"$o":{"1":"one","2":"two","10":10,"b":1,"a":3}},"queryStatements":[],"mutationStatements":[]}},{"at":60,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[{"code":"missing-required","component":"Card","path":"/children","message":"missing required field \"/children\" — signature: Card(children*: array, title: string)","statementId":"root"}]},"stateDeclarations":{"$o":{"1":"one","2":"two","10":10,"b":1,"a":3}},"queryStatements":[],"mutationStatements":[]}},{"at":66,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[{"code":"missing-required","component":"Text","path":"/text","message":"missing required field \"/text\" — signature: Text(text*: string, size: \"s\"|\"m\"|\"l\")","statementId":"root"}]},"stateDeclarations":{"$o":{"1":"one","2":"two","10":10,"b":1,"a":3}},"queryStatements":[],"mutationStatements":[]}},{"at":72,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"x"},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{"$o":{"1":"one","2":"two","10":10,"b":1,"a":3}},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"missing-commas","schema":"test","input":"root = Card([Text(\"a\") Text(\"b\"), Text(\"c\"),])","checkpoints":[{"at":4,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":8,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":["C"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":12,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"missing-required","component":"Card","path":"/children","message":"missing required field \"/children\" — signature: Card(children*: array, title: string)","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":16,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["Tex"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":20,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"a"},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":24,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"a"},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["T"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":28,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"a"},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"missing-required","component":"Text","path":"/text","message":"missing required field \"/text\" — signature: Text(text*: string, size: \"s\"|\"m\"|\"l\")","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":32,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"a"},"partial":true,"hasDynamicProps":false},{"type":"element","typeName":"Text","props":{"text":"b"},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":36,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"a"},"partial":true,"hasDynamicProps":false},{"type":"element","typeName":"Text","props":{"text":"b"},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["Te"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":40,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"a"},"partial":true,"hasDynamicProps":false},{"type":"element","typeName":"Text","props":{"text":"b"},"partial":true,"hasDynamicProps":false},{"type":"element","typeName":"Text","props":{"text":""},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":44,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"a"},"partial":true,"hasDynamicProps":false},{"type":"element","typeName":"Text","props":{"text":"b"},"partial":true,"hasDynamicProps":false},{"type":"element","typeName":"Text","props":{"text":"c"},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":46,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"a"},"partial":false,"hasDynamicProps":false},{"type":"element","typeName":"Text","props":{"text":"b"},"partial":false,"hasDynamicProps":false},{"type":"element","typeName":"Text","props":{"text":"c"},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"lowercase-builtin-name","schema":"test","input":"root = Card([Text(@first([1]))])","checkpoints":[{"at":3,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":6,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":9,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":["Ca"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":12,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"missing-required","component":"Card","path":"/children","message":"missing required field \"/children\" — signature: Card(children*: array, title: string)","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":15,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["Te"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":18,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"missing-required","component":"Text","path":"/text","message":"missing required field \"/text\" — signature: Text(text*: string, size: \"s\"|\"m\"|\"l\")","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":21,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["fi"],"orphaned":[],"statementCount":1,"errors":[{"code":"null-required","component":"Text","path":"/text","message":"required field \"/text\" cannot be null — signature: Text(text*: string, size: \"s\"|\"m\"|\"l\")","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":24,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["first"],"orphaned":[],"statementCount":1,"errors":[{"code":"null-required","component":"Text","path":"/text","message":"required field \"/text\" cannot be null — signature: Text(text*: string, size: \"s\"|\"m\"|\"l\")","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":27,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"unknown-component","component":"first","path":"","message":"Unknown component \"first\" — not found in catalog or builtins. Available components: Card, Text, Button, Input, Chart, Tagged","statementId":"root"},{"code":"null-required","component":"Text","path":"/text","message":"required field \"/text\" cannot be null — signature: Text(text*: string, size: \"s\"|\"m\"|\"l\")","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":30,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"unknown-component","component":"first","path":"","message":"Unknown component \"first\" — not found in catalog or builtins. Available components: Card, Text, Button, Input, Chart, Tagged","statementId":"root"},{"code":"null-required","component":"Text","path":"/text","message":"required field \"/text\" cannot be null — signature: Text(text*: string, size: \"s\"|\"m\"|\"l\")","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":32,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"unknown-component","component":"first","path":"","message":"Unknown component \"first\" — not found in catalog or builtins. Available components: Card, Text, Button, Input, Chart, Tagged","statementId":"root"},{"code":"null-required","component":"Text","path":"/text","message":"required field \"/text\" cannot be null — signature: Text(text*: string, size: \"s\"|\"m\"|\"l\")","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"unknown-tokens","schema":"test","input":"root = Card([Text(\"a\")]) ; ~ ^\n$ = 3\n= 4\nfoo","checkpoints":[{"at":4,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":8,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":["C"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":12,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"missing-required","component":"Card","path":"/children","message":"missing required field \"/children\" — signature: Card(children*: array, title: string)","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":16,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["Tex"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":20,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"a"},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":24,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"a"},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":28,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"a"},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":32,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"a"},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":36,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"a"},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":40,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"a"},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":44,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"a"},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"combining-characters","schema":"test","input":"root = Card([Text(\"e\\u0301 cafe\\u0301\"), Text(\"한국어\"), Text(\"ok\")])","checkpoints":[{"at":6,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":12,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"missing-required","component":"Card","path":"/children","message":"missing required field \"/children\" — signature: Card(children*: array, title: string)","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":18,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"missing-required","component":"Text","path":"/text","message":"missing required field \"/text\" — signature: Text(text*: string, size: \"s\"|\"m\"|\"l\")","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":24,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"e\\u03"},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":30,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"é caf"},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":36,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"e\\u0301 cafe\\u030"},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":42,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"é café"},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["T"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":48,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"é café"},"partial":true,"hasDynamicProps":false},{"type":"element","typeName":"Text","props":{"text":"한"},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":54,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"é café"},"partial":true,"hasDynamicProps":false},{"type":"element","typeName":"Text","props":{"text":"한국어"},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":60,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"é café"},"partial":true,"hasDynamicProps":false},{"type":"element","typeName":"Text","props":{"text":"한국어"},"partial":true,"hasDynamicProps":false},{"type":"element","typeName":"Text","props":{"text":""},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":66,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"é café"},"partial":false,"hasDynamicProps":false},{"type":"element","typeName":"Text","props":{"text":"한국어"},"partial":false,"hasDynamicProps":false},{"type":"element","typeName":"Text","props":{"text":"ok"},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"truncated-mid-call","schema":"test","input":"root = Card([a, b])\na = Text(\"one\")\nb = Button(\"tw","checkpoints":[{"at":5,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":10,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":["Car"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":15,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["a"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":20,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["a","b"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":25,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["T","b"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":30,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":""},"partial":true,"hasDynamicProps":false,"statementId":"a"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["b"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":35,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"one"},"partial":false,"hasDynamicProps":false,"statementId":"a"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["b"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":40,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"one"},"partial":false,"hasDynamicProps":false,"statementId":"a"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["b"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":45,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"one"},"partial":false,"hasDynamicProps":false,"statementId":"a"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["Butto"],"orphaned":[],"statementCount":3,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":50,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"one"},"partial":true,"hasDynamicProps":false,"statementId":"a"},{"type":"element","typeName":"Button","props":{"label":"tw"},"partial":true,"hasDynamicProps":false,"statementId":"b"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":3,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"truncated-mid-object","schema":"test","input":"root = Card([Chart([\"a\"], [1], {unit: \"k","checkpoints":[{"at":4,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":8,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":["C"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":12,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"missing-required","component":"Card","path":"/children","message":"missing required field \"/children\" — signature: Card(children*: array, title: string)","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":16,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["Cha"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":20,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"missing-required","component":"Chart","path":"/values","message":"missing required field \"/values\" — signature: Chart(labels*: array, values*: array, meta: object)","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":24,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"missing-required","component":"Chart","path":"/values","message":"missing required field \"/values\" — signature: Chart(labels*: array, values*: array, meta: object)","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":28,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Chart","props":{"labels":["a"],"values":[1]},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":32,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Chart","props":{"labels":["a"],"values":[1],"meta":{}},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":36,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Chart","props":{"labels":["a"],"values":[1],"meta":{"unit":null,"?":null}},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":40,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Chart","props":{"labels":["a"],"values":[1],"meta":{"unit":"k"}},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"data-in-component-slot","schema":"test","input":"root = Card([Text(\"hi\"), { text: \"a\" }, \"b\", 3, [Text(\"x\")], true])","checkpoints":[{"at":6,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":12,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"missing-required","component":"Card","path":"/children","message":"missing required field \"/children\" — signature: Card(children*: array, title: string)","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":18,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"missing-required","component":"Text","path":"/text","message":"missing required field \"/text\" — signature: Text(text*: string, size: \"s\"|\"m\"|\"l\")","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":24,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"hi"},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":30,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"hi"},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"type-mismatch","component":"Card","path":"/children/1","message":"field \"/children/1\" expects Text | Button | Chart but got plain object","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":36,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"hi"},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"type-mismatch","component":"Card","path":"/children/1","message":"field \"/children/1\" expects Text | Button | Chart but got plain object","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":42,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"hi"},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"type-mismatch","component":"Card","path":"/children/1","message":"field \"/children/1\" expects Text | Button | Chart but got plain object","statementId":"root"},{"code":"type-mismatch","component":"Card","path":"/children/2","message":"field \"/children/2\" expects Text | Button | Chart but got string","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":48,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"hi"},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"type-mismatch","component":"Card","path":"/children/1","message":"field \"/children/1\" expects Text | Button | Chart but got plain object","statementId":"root"},{"code":"type-mismatch","component":"Card","path":"/children/2","message":"field \"/children/2\" expects Text | Button | Chart but got string","statementId":"root"},{"code":"type-mismatch","component":"Card","path":"/children/3","message":"field \"/children/3\" expects Text | Button | Chart but got number","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":54,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"hi"},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"missing-required","component":"Text","path":"/text","message":"missing required field \"/text\" — signature: Text(text*: string, size: \"s\"|\"m\"|\"l\")","statementId":"root"},{"code":"type-mismatch","component":"Card","path":"/children/1","message":"field \"/children/1\" expects Text | Button | Chart but got plain object","statementId":"root"},{"code":"type-mismatch","component":"Card","path":"/children/2","message":"field \"/children/2\" expects Text | Button | Chart but got string","statementId":"root"},{"code":"type-mismatch","component":"Card","path":"/children/3","message":"field \"/children/3\" expects Text | Button | Chart but got number","statementId":"root"},{"code":"type-mismatch","component":"Card","path":"/children/4","message":"field \"/children/4\" expects Text | Button | Chart but got array","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":60,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"hi"},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"type-mismatch","component":"Card","path":"/children/1","message":"field \"/children/1\" expects Text | Button | Chart but got plain object","statementId":"root"},{"code":"type-mismatch","component":"Card","path":"/children/2","message":"field \"/children/2\" expects Text | Button | Chart but got string","statementId":"root"},{"code":"type-mismatch","component":"Card","path":"/children/3","message":"field \"/children/3\" expects Text | Button | Chart but got number","statementId":"root"},{"code":"type-mismatch","component":"Card","path":"/children/4","message":"field \"/children/4\" expects Text | Button | Chart but got array","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":66,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"hi"},"partial":true,"hasDynamicProps":false}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"type-mismatch","component":"Card","path":"/children/1","message":"field \"/children/1\" expects Text | Button | Chart but got plain object","statementId":"root"},{"code":"type-mismatch","component":"Card","path":"/children/2","message":"field \"/children/2\" expects Text | Button | Chart but got string","statementId":"root"},{"code":"type-mismatch","component":"Card","path":"/children/3","message":"field \"/children/3\" expects Text | Button | Chart but got number","statementId":"root"},{"code":"type-mismatch","component":"Card","path":"/children/4","message":"field \"/children/4\" expects Text | Button | Chart but got array","statementId":"root"},{"code":"type-mismatch","component":"Card","path":"/children/5","message":"field \"/children/5\" expects Text | Button | Chart but got boolean","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":67,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"Text","props":{"text":"hi"},"partial":false,"hasDynamicProps":false}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"type-mismatch","component":"Card","path":"/children/1","message":"field \"/children/1\" expects Text | Button | Chart but got plain object","statementId":"root"},{"code":"type-mismatch","component":"Card","path":"/children/2","message":"field \"/children/2\" expects Text | Button | Chart but got string","statementId":"root"},{"code":"type-mismatch","component":"Card","path":"/children/3","message":"field \"/children/3\" expects Text | Button | Chart but got number","statementId":"root"},{"code":"type-mismatch","component":"Card","path":"/children/4","message":"field \"/children/4\" expects Text | Button | Chart but got array","statementId":"root"},{"code":"type-mismatch","component":"Card","path":"/children/5","message":"field \"/children/5\" expects Text | Button | Chart but got boolean","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"data-in-component-slot-via-ref","schema":"test","input":"root = Card(items)\nitems = [{ text: \"a\" }]","checkpoints":[{"at":4,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":8,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":["C"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":12,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"missing-required","component":"Card","path":"/children","message":"missing required field \"/children\" — signature: Card(children*: array, title: string)","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":16,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":["item"],"orphaned":[],"statementCount":1,"errors":[{"code":"null-required","component":"Card","path":"/children","message":"required field \"/children\" cannot be null — signature: Card(children*: array, title: string)","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":20,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":["items"],"orphaned":[],"statementCount":1,"errors":[{"code":"null-required","component":"Card","path":"/children","message":"required field \"/children\" cannot be null — signature: Card(children*: array, title: string)","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":24,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":["items"],"orphaned":[],"statementCount":1,"errors":[{"code":"null-required","component":"Card","path":"/children","message":"required field \"/children\" cannot be null — signature: Card(children*: array, title: string)","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":28,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":32,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[{"code":"type-mismatch","component":"Card","path":"/children/0","message":"field \"/children/0\" expects Text | Button | Chart but got plain object","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":36,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[{"code":"type-mismatch","component":"Card","path":"/children/0","message":"field \"/children/0\" expects Text | Button | Chart but got plain object","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":40,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[{"code":"type-mismatch","component":"Card","path":"/children/0","message":"field \"/children/0\" expects Text | Button | Chart but got plain object","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":42,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":2,"errors":[{"code":"type-mismatch","component":"Card","path":"/children/0","message":"field \"/children/0\" expects Text | Button | Chart but got plain object","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"hoisted-data-def","schema":"hoisted","input":"root = Tree({ label: \"a\" })","checkpoints":[{"at":3,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":6,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":9,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":["Tr"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":12,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[{"code":"missing-required","component":"Tree","path":"/node","message":"missing required field \"/node\" — signature: Tree(node*: __schema0)","statementId":"root"}]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":15,"expected":{"root":{"type":"element","typeName":"Tree","props":{"node":{"l":null,"?":null}},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":18,"expected":{"root":{"type":"element","typeName":"Tree","props":{"node":{"labe":null,"?":null}},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":21,"expected":{"root":{"type":"element","typeName":"Tree","props":{"node":{"label":null,"?":null}},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":24,"expected":{"root":{"type":"element","typeName":"Tree","props":{"node":{"label":"a"}},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":27,"expected":{"root":{"type":"element","typeName":"Tree","props":{"node":{"label":"a"}},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]}, +{"name":"cloud-envelope","schema":"chat","input":"]]>openui:content?thesys=true\n```openui-lang\nroot = Card([h, t])\nh = CardHeader(\"Q3\", \"Revenue\")\nt = TextContent(\"Up 12%\")\n```\n]]>openui:end","checkpoints":[{"at":12,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":24,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":36,"expected":{"root":null,"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":48,"expected":{"root":null,"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":0,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":60,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["h"],"orphaned":[],"statementCount":1,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":72,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["Car","t"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":84,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Q3"},"partial":true,"hasDynamicProps":false,"statementId":"h"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":["t"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":96,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Q3","subtitle":"Revenue"},"partial":false,"hasDynamicProps":false,"statementId":"h"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["t"],"orphaned":[],"statementCount":2,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":108,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Q3","subtitle":"Revenue"},"partial":false,"hasDynamicProps":false,"statementId":"h"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":["TextCon"],"orphaned":[],"statementCount":3,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":120,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Q3","subtitle":"Revenue"},"partial":true,"hasDynamicProps":false,"statementId":"h"},{"type":"element","typeName":"TextContent","props":{"text":"Up 12%"},"partial":true,"hasDynamicProps":false,"statementId":"t"}]},"partial":true,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":true,"unresolved":[],"orphaned":[],"statementCount":3,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":132,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Q3","subtitle":"Revenue"},"partial":false,"hasDynamicProps":false,"statementId":"h"},{"type":"element","typeName":"TextContent","props":{"text":"Up 12%"},"partial":false,"hasDynamicProps":false,"statementId":"t"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":3,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}},{"at":140,"expected":{"root":{"type":"element","typeName":"Card","props":{"children":[{"type":"element","typeName":"CardHeader","props":{"title":"Q3","subtitle":"Revenue"},"partial":false,"hasDynamicProps":false,"statementId":"h"},{"type":"element","typeName":"TextContent","props":{"text":"Up 12%"},"partial":false,"hasDynamicProps":false,"statementId":"t"}]},"partial":false,"hasDynamicProps":false,"statementId":"root"},"meta":{"incomplete":false,"unresolved":[],"orphaned":[],"statementCount":3,"errors":[]},"stateDeclarations":{},"queryStatements":[],"mutationStatements":[]}}]} +] diff --git a/packages/swift-lang/Tests/OpenUILangTests/JSSemanticsTests.swift b/packages/swift-lang/Tests/OpenUILangTests/JSSemanticsTests.swift new file mode 100644 index 0000000000..d4ba02da08 --- /dev/null +++ b/packages/swift-lang/Tests/OpenUILangTests/JSSemanticsTests.swift @@ -0,0 +1,64 @@ +import Testing + +@testable import OpenUILang + +/// Expected values were produced by Node.js, so these pin the port to real +/// JavaScript behaviour rather than to what Swift does by default. +@Suite struct JSSemanticsTests { + @Test(arguments: [ + (0.0, "0"), (-0.0, "0"), (1, "1"), (-1, "-1"), (0.1 + 0.2, "0.30000000000000004"), + (100, "100"), (1e21, "1e+21"), (1e-7, "1e-7"), (1.5e-7, "1.5e-7"), (123e-20, "1.23e-18"), + (1e20, "100000000000000000000"), (1.2345678901234568e20, "123456789012345680000"), + (0.000001, "0.000001"), (5e-324, "5e-324"), + (1.797_693_134_862_315_7e308, "1.7976931348623157e+308"), (.nan, "NaN"), + (.infinity, "Infinity"), (-.infinity, "-Infinity"), (2.5, "2.5"), (-2.5e-3, "-0.0025"), + (1.0 / 3, "0.3333333333333333"), + ]) + func numberToString(_ value: Double, _ expected: String) { + #expect(jsNumberToString(value) == expected) + } + + @Test(arguments: [ + (" 12 ", 12.0), ("0x1F", 31), ("0b101", 5), ("0o17", 15), ("", 0), (" ", 0), (".5", 0.5), + ("5.", 5), ("Infinity", .infinity), ("-Infinity", -.infinity), ("+3", 3), ("1e+2", 100), + ("1E-2", 0.01), ("\u{00A0}12\u{FEFF}", 12), + ]) + func stringToNumber(_ text: String, _ expected: Double) { + #expect(jsStringToNumber(text) == expected) + } + + @Test(arguments: ["1e", "1_000", "12px", "-0x1F", "--1", "0.0.1"]) + func stringToNumberNaN(_ text: String) { + #expect(jsStringToNumber(text).isNaN) + } + + @Test func looseEquality() { + let pairs: [(OpenUIValue, OpenUIValue, Bool)] = [ + (1, "1", true), ("1", 1, true), (0, "", true), (0, "0", true), ("", 0, true), + (.null, .undefined, true), (.null, 0, false), (.undefined, 0, false), (true, 1, true), + (true, "1", true), (false, "", true), ("true", true, false), ([1], 1, true), + ([1, 2], "1,2", true), (["a": 1], "[object Object]", true), + (.number(.nan), .number(.nan), false), + ("a", "a", true), ([], "", true), ([.null], "", true), (2, true, false), + ] + for (a, b, expected) in pairs { + #expect(a.looselyEquals(b) == expected, "\(a) == \(b)") + } + } + + @Test func truthiness() { + let falsy: [OpenUIValue] = [.undefined, .null, false, 0, -0.0, .number(.nan), ""] + let truthy: [OpenUIValue] = [true, 1, -1, "0", " ", [], [:]] + for value in falsy { #expect(!value.isTruthy, "\(value)") } + for value in truthy { #expect(value.isTruthy, "\(value)") } + } + + @Test func objectKeyOrder() { + var object = OpenUIObject() + for key in ["b", "2", "a", "1", "10", "01"] { object[key] = .string(key) } + // Integer-like keys first in ascending order, then insertion order ("01" is not canonical). + #expect(object.keys == ["1", "2", "10", "b", "a", "01"]) + object["2"] = nil + #expect(object.keys == ["1", "10", "b", "a", "01"]) + } +} diff --git a/packages/swift-lang/Tests/OpenUILangTests/LibraryComponentsTests.swift b/packages/swift-lang/Tests/OpenUILangTests/LibraryComponentsTests.swift new file mode 100644 index 0000000000..7010e2933e --- /dev/null +++ b/packages/swift-lang/Tests/OpenUILangTests/LibraryComponentsTests.swift @@ -0,0 +1,107 @@ +import Testing + +@testable import OpenUILang + +/// The component schemas must match react-ui's Zod schemas, for both +/// openuiChatLibrary (`ChatComponents`) and openuiLibrary (`OpenUIComponents`), +/// so one backend prompt drives both the web and native renderers. +@Suite struct LibraryComponentsTests { + /// One of react-ui's libraries: its fixture key, root and Swift schemas. + struct Subject { + let key: String + let root: String + let all: [ComponentSchema] + let groups: [ComponentGroup] + let promptOptions: PromptOptions + + var tsDefs: OpenUIValue { Fixtures.schemas[key]["schema"]["$defs"] } + var tsSpec: OpenUIValue { Fixtures.load("\(key)-spec") } + var library: Library { + Library( + components: all.map { ComponentDefinition($0, content: ()) }, root: root, + componentGroups: groups) + } + } + + static func subject(_ key: String) -> Subject { + key == "chat" + ? Subject( + key: "chat", root: "Card", all: ChatComponents.all, groups: ChatComponents.groups, + promptOptions: ChatComponents.promptOptions) + : Subject( + key: "openui", root: "Stack", all: OpenUIComponents.all, groups: OpenUIComponents.groups, + promptOptions: OpenUIComponents.promptOptions) + } + + static let libraries = ["chat", "openui"] + static let components: [(String, String)] = libraries.flatMap { key in + subject(key).all.map { (key, $0.name) } + } + + @Test(arguments: components) + func matchesReactUI(_ key: String, _ name: String) { + let subject = Self.subject(key) + let schema = subject.all.first { $0.name == name }! + let expected = subject.tsDefs[name] + let diff = firstDifference(normalized(schema.jsonSchema), normalized(expected)) + #expect(diff == nil, "\(key) \(name): \(diff ?? "")") + // Property order is positional-argument order, so it must match exactly. + #expect( + schema.jsonSchema["properties"].objectValue?.keys == expected["properties"].objectValue?.keys) + #expect(schema.signature == subject.tsSpec["components"][name]["signature"].stringValue) + } + + @Test(arguments: libraries) + func coversEveryComponentInLibraryOrder(_ key: String) { + let subject = Self.subject(key) + #expect(subject.all.map(\.name) == subject.tsSpec["components"].objectValue?.keys) + } + + @Test(arguments: libraries) + func libraryJSONSchemaMatchesByteForByte(_ key: String) { + // Key order included: top-level properties follow library order, $defs + // follow Zod's depth-first discovery order. + let schema = JSON.stringify(Self.subject(key).library.toJSONSchema()) + let expected = JSON.stringify(Fixtures.schemas[key]["schema"]) + #expect(schema == expected, "\(firstLineDifference(schema, expected) ?? "")") + } + + @Test(arguments: libraries) + func bindingsMatchReactiveProps(_ key: String) { + let reactive = Fixtures.schemas[key]["reactive"].objectValue ?? OpenUIObject() + for schema in Self.subject(key).all { + let expected = (reactive[schema.name]?.arrayValue ?? []).compactMap(\.stringValue) + #expect(schema.props.filter(\.isBinding).map(\.name) == expected, "\(key) \(schema.name)") + } + } + + @Test(arguments: libraries) + func groupsMatchReactUI(_ key: String) { + let subject = Self.subject(key) + let expected = (subject.tsSpec["componentGroups"].arrayValue ?? []).map { group in + ComponentGroup( + name: group["name"].stringValue ?? "", + components: (group["components"].arrayValue ?? []).compactMap(\.stringValue), + notes: group["notes"].arrayValue?.compactMap(\.stringValue)) + } + #expect(subject.groups == expected) + } + + @Test(arguments: libraries) + func promptWithItsOptionsMatchesLangCore(_ key: String) { + // What react-ui apps send: the library with its prompt options. + let subject = Self.subject(key) + let fixture = FixtureCase.all("prompts").first { $0.name == "\(key)-options" }! + let prompt = subject.library.prompt(subject.promptOptions) + let expected = fixture.value["expected"].stringValue! + #expect(prompt == expected, "\(firstLineDifference(prompt, expected) ?? "")") + } + + @Test(arguments: libraries) + func promptMatchesLangCore(_ key: String) { + let fixture = FixtureCase.all("prompts").first { $0.name == "\(key)-default" }! + let prompt = Self.subject(key).library.prompt() + let expected = fixture.value["expected"].stringValue! + #expect(prompt == expected, "\(firstLineDifference(prompt, expected) ?? "")") + } +} diff --git a/packages/swift-lang/Tests/OpenUILangTests/ObservabilityTests.swift b/packages/swift-lang/Tests/OpenUILangTests/ObservabilityTests.swift new file mode 100644 index 0000000000..5b411303dd --- /dev/null +++ b/packages/swift-lang/Tests/OpenUILangTests/ObservabilityTests.swift @@ -0,0 +1,161 @@ +import Foundation +import Testing + +@testable import OpenUILang + +/// react-lang's streaming observability tests, ported. +@Suite struct StreamObservationTests { + typealias Update = StreamObservation.Update + + @Test func noEventForContentThatWasNeverStreamed() { + var state = StreamObservation() + var calls = 0 + let update = state.advance( + isStreaming: false, response: #"root = Card("done")"#, settledErrorKey: "[]", + idFactory: { + calls += 1 + return "stream-1" + }) + #expect(update == nil) + #expect(calls == 0) + } + + @Test func oneStableIDForEveryUpdateAndTheSettledEvent() { + var state = StreamObservation() + var calls = 0 + let id = { + calls += 1 + return "stream-1" + } + #expect( + state.advance(isStreaming: true, response: "root = Car", settledErrorKey: nil, idFactory: id) + == Update(id: "stream-1", phase: .streaming, updateIndex: 1)) + #expect( + state.advance( + isStreaming: true, response: #"root = Card("done")"#, settledErrorKey: nil, idFactory: id) + == Update(id: "stream-1", phase: .streaming, updateIndex: 2)) + #expect( + state.advance( + isStreaming: false, response: #"root = Card("done")"#, settledErrorKey: "[]", idFactory: id) + == Update(id: "stream-1", phase: .settled, updateIndex: 3)) + #expect(calls == 1) + } + + @Test func emitsOncePerResponseAndUnchangedSettledSnapshot() { + var state = StreamObservation() + let id = { "stream-1" } + #expect( + state.advance(isStreaming: true, response: "root = Car", settledErrorKey: nil, idFactory: id) + != nil) + #expect( + state.advance(isStreaming: true, response: "root = Car", settledErrorKey: nil, idFactory: id) + == nil) + #expect( + state.advance( + isStreaming: false, response: "root = Car", settledErrorKey: "[]", idFactory: id) != nil) + #expect( + state.advance( + isStreaming: false, response: "root = Car", settledErrorKey: "[]", idFactory: id) == nil) + } + + @Test func startsANewIDWhenASettledRendererStreamsAgain() { + var state = StreamObservation() + var ids = ["stream-1", "stream-2"] + let id = { ids.removeFirst() } + #expect( + state.advance(isStreaming: true, response: "first", settledErrorKey: nil, idFactory: id) + == Update(id: "stream-1", phase: .streaming, updateIndex: 1)) + #expect( + state.advance(isStreaming: false, response: "first", settledErrorKey: "[]", idFactory: id) + == Update(id: "stream-1", phase: .settled, updateIndex: 2)) + #expect( + state.advance(isStreaming: true, response: "second", settledErrorKey: nil, idFactory: id) + == Update(id: "stream-2", phase: .streaming, updateIndex: 1)) + #expect( + state.advance(isStreaming: false, response: "second", settledErrorKey: "[]", idFactory: id) + == Update(id: "stream-2", phase: .settled, updateIndex: 2)) + #expect(ids.isEmpty) + } + + @Test func republishesSettledWhenTheErrorSnapshotChanges() { + var state = StreamObservation() + let id = { "stream-1" } + _ = state.advance( + isStreaming: true, response: "root = Card()", settledErrorKey: nil, idFactory: id) + _ = state.advance( + isStreaming: false, response: "root = Card()", settledErrorKey: "[]", idFactory: id) + let queryError = #"[{"code":"query-error"}]"# + #expect( + state.advance( + isStreaming: false, response: "root = Card()", settledErrorKey: queryError, idFactory: id) + == Update(id: "stream-1", phase: .settled, updateIndex: 3)) + #expect( + state.advance( + isStreaming: false, response: "root = Card()", settledErrorKey: queryError, idFactory: id) + == nil) + #expect( + state.advance( + isStreaming: false, response: "root = Card()", + settledErrorKey: #"[{"code":"query-error"},{"code":"tool-failed"}]"#, idFactory: id) + == Update(id: "stream-1", phase: .settled, updateIndex: 4)) + } +} + +@Suite struct ObservabilityBusTests { + final class Log: @unchecked Sendable { + private let lock = NSLock() + private var items: [ObservabilityEvent] = [] + func append(_ event: ObservabilityEvent) { lock.withLock { items.append(event) } } + var events: [ObservabilityEvent] { lock.withLock { items } } + } + + @Test func deliversByLevelAndToListenAll() { + let bus = Observability() + let errors = Log() + let all = Log() + let stopErrors = bus.listen([.error]) { errors.append($0) } + let stopAll = bus.listenAll { all.append($0) } + bus.info(["kind": "a"]) + bus.error(["kind": "b"]) + #expect(errors.events.map(\.detail["kind"]) == ["b"]) + #expect(all.events.map(\.level) == [.info, .error]) + stopErrors() + stopAll() + bus.error(["kind": "c"]) + #expect(errors.events.count == 1) + #expect(!bus.hasListeners) + } + + /// The runtime publishes react-lang's stream events: one per streamed + /// chunk, then the settled response with its errors. + @MainActor + @Test func runtimePublishesTheStreamLifecycle() { + let log = Log() + // The bus is shared with tests running in parallel; keep only ours. + let marker = "observability-\(UUID().uuidString)" + let stop = Observability.shared.listenAll { event in + if event.detail["response"]?.stringValue?.contains(marker) == true { log.append(event) } + } + defer { stop() } + + let runtime = OpenUIRuntime(library: FixtureLibrary(schemaName: "test")) + let full = #"root = Card([Text("\#(marker)"), Mystery()])"# + runtime.update(response: #"root = Card([Text("\#(marker)""#, isStreaming: true) + runtime.update(response: full, isStreaming: true) + runtime.update(response: full, isStreaming: true) + runtime.update(response: full, isStreaming: false) + + let events = log.events.map { (level: $0.level, detail: OpenUIValue.object($0.detail)) } + #expect(events.map { $0.detail["phase"] } == ["streaming", "streaming", "settled"]) + #expect(Set(events.compactMap { $0.detail["id"].stringValue }).count == 1) + #expect(events.map { $0.detail["updateIndex"] } == [1, 2, 3]) + let settled = events.last! + #expect(settled.detail["kind"] == .string(streamEventKind)) + #expect(settled.level == .error) + #expect(settled.detail["errorCount"] == 1) + #expect(settled.detail["errors"].arrayValue?.first?["code"] == "unknown-component") + #expect(settled.detail["parser"]["statementCount"] == 1) + #expect(settled.detail["message"] == "OpenUI Lang settled with 1 error") + #expect(settled.detail["durationMs"].numberValue != nil) + } +} diff --git a/packages/swift-lang/Tests/OpenUILangTests/ParserConformanceTests.swift b/packages/swift-lang/Tests/OpenUILangTests/ParserConformanceTests.swift new file mode 100644 index 0000000000..f754449276 --- /dev/null +++ b/packages/swift-lang/Tests/OpenUILangTests/ParserConformanceTests.swift @@ -0,0 +1,47 @@ +import Testing + +@testable import OpenUILang + +/// The Swift parser must produce exactly what lang-core produces for the same input. +@Suite struct ParserConformanceTests { + @Test(arguments: FixtureCase.all("parser")) + func parse(_ fixture: FixtureCase) { + let schema = fixture.value["schema"].stringValue! + let result = OpenUILang.parse( + fixture.value["input"].stringValue!, Fixtures.paramMap(schema), + rootName: Fixtures.rootName(schema)) + let diff = firstDifference( + normalized(result.jsonRepresentation), normalized(fixture.value["expected"])) + #expect(diff == nil, "\(fixture.name): \(diff ?? "")") + } + + @Test(arguments: FixtureCase.all("streaming")) + func streamingPush(_ fixture: FixtureCase) { + let schema = fixture.value["schema"].stringValue! + let input = Array(fixture.value["input"].stringValue!.utf16) + let parser = StreamParser(Fixtures.paramMap(schema), rootName: Fixtures.rootName(schema)) + var last = 0 + for checkpoint in fixture.value["checkpoints"].arrayValue ?? [] { + let at = Int(checkpoint["at"].numberValue!) + let chunk = String(decoding: input[last.. PromptSpec { + let components = (json["components"].objectValue ?? OpenUIObject()).entries.map { name, value in + ( + name: name, + spec: ComponentPromptSpec( + signature: value["signature"].stringValue ?? "", + description: value["description"].stringValue) + ) + } + let groups = json["componentGroups"].arrayValue?.map { group in + ComponentGroup( + name: group["name"].stringValue ?? "", + components: (group["components"].arrayValue ?? []).compactMap(\.stringValue), + notes: group["notes"].arrayValue?.compactMap(\.stringValue)) + } + let tools: [ToolDescriptor]? = json["tools"].arrayValue?.map { tool in + if case .string(let name) = tool { return .name(name) } + return .spec( + ToolSpec( + name: tool["name"].stringValue ?? "", description: tool["description"].stringValue, + inputSchema: tool["inputSchema"], outputSchema: tool["outputSchema"])) + } + func strings(_ key: String) -> [String]? { json[key].arrayValue?.compactMap(\.stringValue) } + return PromptSpec( + root: json["root"].stringValue, components: components, componentGroups: groups, + options: PromptOptions( + preamble: json["preamble"].stringValue, additionalRules: strings("additionalRules"), + examples: strings("examples"), toolExamples: strings("toolExamples"), tools: tools, + editMode: json["editMode"].boolValue, inlineMode: json["inlineMode"].boolValue, + toolCalls: json["toolCalls"].boolValue, bindings: json["bindings"].boolValue)) +} + +/// The first line where two texts differ, for readable failures. +func firstLineDifference(_ actual: String, _ expected: String) -> String? { + let a = actual.split(separator: "\n", omittingEmptySubsequences: false) + let e = expected.split(separator: "\n", omittingEmptySubsequences: false) + for index in 0.." + let rhs = index < e.count ? String(e[index]) : "" + if lhs != rhs { return "line \(index + 1):\n got: \(lhs)\n expected: \(rhs)" } + } + return nil +} + +@Suite struct PromptConformanceTests { + @Test(arguments: FixtureCase.all("prompts")) + func generatePromptMatchesLangCore(_ fixture: FixtureCase) { + let prompt = generatePrompt(promptSpec(from: fixture.value["spec"])) + let expected = fixture.value["expected"].stringValue! + #expect(prompt == expected, "\(fixture.name): \(firstLineDifference(prompt, expected) ?? "")") + } +} + +/// Swift-defined components must serialize like the Zod-defined originals. +@Suite struct LibraryDSLTests { + static let rules: PropType = .object( + ["required", "email", "url", "numeric"].map { Prop($0, .boolean, .optional) } + + ["min", "max", "minLength", "maxLength"].map { Prop($0, .number, .optional) } + + [Prop("pattern", .string, .optional)]) + + static let components: [ComponentSchema] = [ + ComponentSchema( + "TextContent", + description: + "Text block. Supports markdown. Optional size: \"small\" | \"default\" | \"large\" | \"small-heavy\" | \"large-heavy\".", + props: [ + Prop("text", .string), + Prop( + "size", .enumeration(["small", "default", "large", "small-heavy", "large-heavy"]), + .optional), + ]), + ComponentSchema( + "Input", description: "", + props: [ + Prop("name", .string), Prop("placeholder", .string, .optional), + Prop("type", .enumeration(["text", "email", "password", "number", "url"]), .optional), + Prop("rules", rules, .optional), Prop("value", .string, .optional, binding: true), + ]), + ComponentSchema( + "Button", description: "Clickable button", + props: [ + Prop("label", .string), Prop("action", .actionExpression, .optional), + Prop("variant", .enumeration(["primary", "secondary", "tertiary"]), .optional), + Prop("type", .enumeration(["normal", "destructive"]), .optional), + Prop("size", .enumeration(["extra-small", "small", "medium", "large"]), .optional), + ]), + ComponentSchema( + "Col", description: "Column definition — holds label + data array", + props: [ + Prop("label", .string), Prop("data", .any), + Prop("type", .enumeration(["string", "number", "action"]), .optional), + ]), + ComponentSchema( + "CheckBoxGroup", description: "", + props: [ + Prop("name", .string), Prop("items", .array(.component("CheckBoxItem"))), + Prop("rules", rules, .optional), + Prop("value", .record(.boolean), .optional, binding: true), + ]), + ComponentSchema( + "EntityList", + description: + "Two-column key/value rows (left label, right value). size 'default' supports optional header and footer rows; rightVariant 'number' uses tabular numbers.", + props: [ + Prop("rows", .array(entityRow), .defaulted([])), + Prop("size", .enumeration(["small", "default"]), .defaulted("default")), + Prop("header", entityRow, .optional), Prop("footer", entityRow, .optional), + ]), + ComponentSchema( + "Chips", + description: + "A form field of compact selectable chips for choosing one or many short options; the selection is stored under `name`.", + props: [ + Prop("name", .string), + Prop("type", .enumeration(["single", "multiple"]), .defaulted("multiple")), + Prop("items", .array(.component("ChipItem")), .defaulted([])), + Prop("rules", rules, .optional), + Prop("defaultValue", .union([.string, .array(.string)]), .optional), + ]), + ] + + static let entityRow: PropType = .object([ + Prop("left", .string), Prop("right", .string), + Prop("rightVariant", .enumeration(["text", "number"]), .defaulted("text")), + ]) + + static let chatDefs = Fixtures.schemas["chat"]["schema"]["$defs"] + static let chatSpec = Fixtures.load("chat-spec") + + @Test(arguments: components.map(\.name)) + func schemaMatchesZod(_ name: String) { + let component = Self.components.first { $0.name == name }! + let expected = Self.chatDefs[name] + let diff = firstDifference(normalized(component.jsonSchema), normalized(expected)) + #expect(diff == nil, "\(name): \(diff ?? "")") + // Property order is positional-argument order, so it must match exactly. + #expect( + component.jsonSchema["properties"].objectValue?.keys + == expected["properties"].objectValue?.keys) + } + + @Test(arguments: components.map(\.name)) + func signatureMatchesLangCore(_ name: String) { + let component = Self.components.first { $0.name == name }! + #expect(component.signature == Self.chatSpec["components"][name]["signature"].stringValue) + } + + @Test func libraryWiresBindingsRootAndSchema() { + let library = Library( + components: Self.components.map { ComponentDefinition($0, content: ()) }, root: "TextContent") + #expect(library.isReactiveProp("Input", "value")) + #expect(!library.isReactiveProp("Input", "name")) + #expect( + library.paramMap["Button"]?.map(\.name) == ["label", "action", "variant", "type", "size"]) + #expect(library.toJSONSchema()["required"].arrayValue?.count == Self.components.count) + #expect( + library.toSpec()["components"]["Button"]["signature"].stringValue?.hasPrefix( + "Button(label: string, action?: ActionExpression") == true) + + let result = parse( + "root = TextContent(\"hi\", \"large\")", library.paramMap, rootName: library.rootName) + #expect(result.root?.props == ["text": "hi", "size": "large"]) + } + + @Test func promptIncludesActionAndBindingNotes() { + let library = Library(components: Self.components.map { ComponentDefinition($0, content: ()) }) + let prompt = library.prompt() + #expect( + prompt.contains("Props typed `ActionExpression` accept an Action([@steps...]) expression.")) + #expect(prompt.contains("Props marked `$binding` accept a `$variable` reference")) + #expect(prompt.contains("root = Root(...)")) + } +} diff --git a/packages/swift-lang/Tests/OpenUILangTests/QueryAndFormTests.swift b/packages/swift-lang/Tests/OpenUILangTests/QueryAndFormTests.swift new file mode 100644 index 0000000000..b0d4939d18 --- /dev/null +++ b/packages/swift-lang/Tests/OpenUILangTests/QueryAndFormTests.swift @@ -0,0 +1,225 @@ +import Foundation +import Testing + +@testable import OpenUILang + +@MainActor +@Suite struct QueryManagerTests { + /// Waits for in-flight tool calls to settle. + func settle() async { + for _ in 0..<20 { await Task.yield() } + } + + @Test func fetchesQueriesAndFallsBackToDefaults() async { + let provider = FunctionToolProvider([ + "list_users": { args in ["rows": [["name": args["q"] ?? .null]]] } + ]) + let manager = QueryManager(toolProvider: provider) + let node = QueryNode( + statementId: "users", toolName: "list_users", args: ["q": "ada"], defaults: ["rows": []], + refreshInterval: nil, deps: nil, complete: true) + + manager.evaluateQueries([node]) + #expect(manager.result("users") == ["rows": []]) + #expect(manager.isLoading("users")) + + await settle() + #expect(manager.result("users") == ["rows": [["name": "ada"]]]) + #expect(!manager.isAnyLoading) + #expect(manager.result("unknown") == .null) + } + + /// A refresh interval comes straight from the response, so one too large + /// to schedule (or infinite) mustn't bring the app down. + @Test func survivesHugeRefreshIntervals() async { + let manager = QueryManager(toolProvider: FunctionToolProvider(["tick": { _ in 1 }])) + for interval in [Double.infinity, 1e30] { + manager.evaluateQueries([ + QueryNode( + statementId: "q", toolName: "tick", args: [:], defaults: 0, refreshInterval: interval, + deps: nil, complete: true) + ]) + await settle() + } + #expect(manager.result("q") == 1) + manager.dispose() + } + + @Test func reportsMissingTools() async { + let manager = QueryManager(toolProvider: FunctionToolProvider(["other": { _ in .null }])) + manager.evaluateQueries([ + QueryNode( + statementId: "q", toolName: "nope", args: [:], defaults: .null, refreshInterval: nil, + deps: nil, complete: true) + ]) + await settle() + #expect(manager.snapshot.errors.first?.code == "tool-not-found") + #expect(manager.snapshot.errors.first?.hint == "Available tools: other") + } + + @Test func refetchRecoversFromAFailedQuery() async { + struct Unavailable: Error {} + final class Calls: @unchecked Sendable { var count = 0 } + let calls = Calls() + let manager = QueryManager( + toolProvider: FunctionToolProvider([ + "flaky": { _ in + calls.count += 1 + if calls.count == 1 { throw Unavailable() } + return ["ok": true] + } + ])) + manager.evaluateQueries([ + QueryNode( + statementId: "q", toolName: "flaky", args: [:], defaults: ["ok": false], + refreshInterval: nil, deps: nil, complete: true) + ]) + await settle() + #expect(manager.snapshot.errors.map(\.code) == ["tool-error"]) + #expect(manager.result("q") == ["ok": false]) + + manager.invalidate(["q"]) + #expect(manager.isLoading("q")) + await settle() + #expect(manager.snapshot.errors.isEmpty) + #expect(manager.result("q") == ["ok": true]) + #expect(calls.count == 2) + } + + @Test func mutationSuccessAndFailure() async { + let provider = FunctionToolProvider([ + "save": { args in ["saved": args["name"] ?? .null] }, + "fail": { _ in throw McpToolError(toolErrorText: "nope") }, + ]) + let manager = QueryManager(toolProvider: provider) + manager.registerMutations([ + MutationNode(statementId: "save", toolName: "save"), + MutationNode(statementId: "boom", toolName: "fail"), + ]) + #expect(manager.mutationResult("save") == ["status": "idle"]) + + #expect(await manager.fireMutation("save", args: ["name": "ada"])) + #expect(manager.mutationResult("save") == ["status": "success", "data": ["saved": "ada"]]) + + #expect(await manager.fireMutation("boom", args: [:]) == false) + #expect(manager.mutationResult("boom")?["status"] == "error") + #expect(manager.snapshot.errors.first?.code == "mcp-error") + } + + /// Errors read like lang-core's `err.message`: a Cocoa error's localized + /// description rather than its `Error Domain=… Code=0 "(null)"` debug form, + /// and a `LocalizedError`'s own description. + @Test func toolErrorsUseTheErrorsMessage() async { + struct Offline: LocalizedError { + var errorDescription: String? { "You're offline." } + } + let cocoa = NSError(domain: "kCLErrorDomain", code: 0) + let provider = FunctionToolProvider([ + "locate": { _ in throw cocoa }, + "sync": { _ in throw Offline() }, + ]) + let manager = QueryManager(toolProvider: provider) + manager.registerMutations([ + MutationNode(statementId: "locate", toolName: "locate"), + MutationNode(statementId: "sync", toolName: "sync"), + ]) + _ = await manager.fireMutation("locate", args: [:]) + _ = await manager.fireMutation("sync", args: [:]) + let messages = manager.snapshot.errors.map(\.message) + #expect(messages.contains("Mutation \"locate\" failed: \(cocoa.localizedDescription)")) + #expect(messages.contains("Mutation \"sync\" failed: You're offline.")) + #expect(!messages.joined().contains("Error Domain")) + #expect(manager.mutationResult("sync")?["error"] == "You're offline.") + } + + @Test func failedMutationHaltsTheActionPlan() async { + let provider = FunctionToolProvider(["fail": { _ in throw ToolNotFoundError(toolName: "x") }]) + let runtime = OpenUIRuntime(library: FixtureLibrary(schemaName: "test"), toolProvider: provider) + var events: [ActionEvent] = [] + runtime.onAction = { events.append($0) } + runtime.update( + response: """ + save = Mutation("fail", {}) + root = Card([Button("Save", Action([@Run(save), @ToAssistant("Saved")]))]) + """, isStreaming: false) + let action = runtime.evaluatedRoot()?.props["children"]?.arrayValue?.first?.elementValue? + .props["action"] + + await runtime.triggerAction("Save", action: action) + #expect(events.isEmpty) + } + + @Test func extractsMcpResults() throws { + #expect( + try extractToolResult(["content": [["type": "text", "text": "{\"a\":1}"]]]) == ["a": 1]) + #expect(try extractToolResult(["content": [["type": "text", "text": "plain"]]]) == "plain") + #expect(try extractToolResult(["structuredContent": ["b": 2], "content": []]) == ["b": 2]) + #expect(throws: McpToolError.self) { + try extractToolResult(["isError": true, "content": [["type": "text", "text": "bad"]]]) + } + } + + /// An MCP client as the tool provider, as react-lang's Renderer takes one: + /// raw results are unwrapped, and error results become `mcp-error`. + @Test func mcpClientResultsAreUnwrapped() async throws { + let provider = McpToolProvider { name, arguments in + if name == "fail" { + return ["isError": true, "content": [["type": "text", "text": "quota exceeded"]]] + } + let echo = OpenUIValue.string("{\"echo\":\(JSON.stringify(arguments["q"] ?? .null))}") + return ["content": [["type": "text", "text": echo]]] + } + #expect(try await provider.callTool("search", arguments: ["q": "ada"]) == ["echo": "ada"]) + + let manager = QueryManager(toolProvider: provider) + manager.registerMutations([MutationNode(statementId: "boom", toolName: "fail")]) + #expect(await manager.fireMutation("boom", args: [:]) == false) + #expect(manager.snapshot.errors.first?.code == "mcp-error") + #expect(manager.snapshot.errors.first?.message.contains("quota exceeded") == true) + } +} + +@Suite struct FormRulesTests { + @Test func parsesRules() { + #expect(parseRule("min:8") == ParsedRule(type: "min", arg: 8)) + #expect(parseRule("pattern:^[a-z]:x") == ParsedRule(type: "pattern", arg: "^[a-z]:x")) + #expect(parseRule("minLength:abc") == ParsedRule(type: "minLength", arg: "abc")) + #expect( + parseStructuredRules(["required": true, "email": false, "max": 5, "min": .null]) + == [ParsedRule(type: "required"), ParsedRule(type: "max", arg: 5)]) + } + + @Test func builtInMessages() { + let cases: [(OpenUIValue, [ParsedRule], String?)] = [ + ("", [ParsedRule(type: "required")], "This field is required"), + (["value": ""], [ParsedRule(type: "required")], "This field is required"), + ( + ["a": false, "b": false], [ParsedRule(type: "required")], "At least one option is required" + ), + ("ada@x.io", [ParsedRule(type: "email")], nil), + ("ada@x", [ParsedRule(type: "email")], "Please enter a valid email"), + ("https://openui.com", [ParsedRule(type: "url")], nil), + ("openui.com", [ParsedRule(type: "url")], "Please enter a valid URL"), + ("12px", [ParsedRule(type: "numeric")], nil), + ("abc", [ParsedRule(type: "numeric")], "Must be a number"), + ("3", [ParsedRule(type: "min", arg: 5)], "Must be at least 5"), + (11, [ParsedRule(type: "max", arg: 10.5)], "Must be no more than 10.5"), + ("ab", [ParsedRule(type: "minLength", arg: 3)], "Must be at least 3 characters"), + ("😀😀", [ParsedRule(type: "maxLength", arg: 3)], "Must be no more than 3 characters"), + ("abc", [ParsedRule(type: "pattern", arg: "^[0-9]+$")], "Invalid format"), + ( + "x", [ParsedRule(type: "unknownRule"), ParsedRule(type: "minLength", arg: 2)], + "Must be at least 2 characters" + ), + ] + for (value, rules, expected) in cases { + #expect(validate(value, rules) == expected, "\(value) \(rules)") + } + } + + @Test func customValidatorsWin() { + let custom: [String: ValidatorFn] = ["required": { _, _ in "custom" }] + #expect( + validate("filled", [ParsedRule(type: "required")], customValidators: custom) == "custom") + } +} diff --git a/packages/swift-lang/Tests/OpenUILangTests/RuntimeTests.swift b/packages/swift-lang/Tests/OpenUILangTests/RuntimeTests.swift new file mode 100644 index 0000000000..d00bef74be --- /dev/null +++ b/packages/swift-lang/Tests/OpenUILangTests/RuntimeTests.swift @@ -0,0 +1,192 @@ +import Testing + +@testable import OpenUILang + +/// A library built straight from the test fixture schema. +struct FixtureLibrary: ComponentLibrary { + let schemaName: String + var paramMap: ParamMap { Fixtures.paramMap(schemaName) } + var rootName: String? { Fixtures.rootName(schemaName) } + func isReactiveProp(_ component: String, _ prop: String) -> Bool { + Fixtures.schemaContext(schemaName).isReactiveProp(component, prop) + } +} + +@MainActor +@Suite struct RuntimeTests { + func makeRuntime(_ schema: String = "test", initialState: OpenUIObject? = nil) -> OpenUIRuntime { + OpenUIRuntime(library: FixtureLibrary(schemaName: schema), initialState: initialState) + } + + @Test func streamsAndSeedsStateFromDeclarations() { + let runtime = makeRuntime() + runtime.update(response: "$count = 3\nroot = Card([Text(\"n \" + $co", isStreaming: true) + #expect(runtime.parseResult?.meta.incomplete == true) + #expect(runtime.store.get("$count") == 3) + + runtime.update(response: "$count = 3\nroot = Card([Text(\"n \" + $count)])", isStreaming: false) + #expect( + runtime.evaluatedRoot()?.props["children"]?.arrayValue?.first?.elementValue? + .props["text"] == "n 3") + } + + /// `preload` renders a response before the host is told about it; the first + /// `update` with the same text reports it once, as if nothing came before. + @Test func preloadDefersCallbacksToTheFirstUpdate() { + let runtime = makeRuntime() + var reported = 0 + var stateUpdates = 0 + runtime.onParseResult = { _ in reported += 1 } + runtime.onStateUpdate = { _ in stateUpdates += 1 } + let response = "$count = 3\nroot = Card([Text(\"n \" + $count)])" + + runtime.preload(response: response, isStreaming: false) + #expect(reported == 0) + #expect(stateUpdates == 0) + #expect(runtime.store.get("$count") == 3) + #expect( + runtime.evaluatedRoot()?.props["children"]?.arrayValue?.first?.elementValue? + .props["text"] == "n 3") + + runtime.update(response: response, isStreaming: false) + #expect(reported == 1) + runtime.update(response: response, isStreaming: false) + #expect(reported == 1) + #expect(stateUpdates == 0) + } + + @Test func keepsUserChangesWhenDeclarationsChange() { + let runtime = makeRuntime() + runtime.update(response: "$name = \"\"\nroot = Card([Text($name)])", isStreaming: true) + runtime.store.set("$name", "Ada") + // A new declaration arrives while streaming; the user's value must survive. + runtime.update( + response: "$name = \"\"\n$other = 1\nroot = Card([Text($name)])", isStreaming: false) + #expect(runtime.store.get("$name") == "Ada") + #expect(runtime.store.get("$other") == 1) + } + + @Test func initialStateSplitsBindingsAndFormData() { + let runtime = makeRuntime(initialState: ["$name": "Restored", "contact": ["email": "a@b.co"]]) + runtime.update(response: "$name = \"\"\nroot = Card([Text($name)])", isStreaming: false) + #expect(runtime.store.get("$name") == "Restored") + #expect(runtime.store.get("contact") == ["email": "a@b.co"]) + } + + @Test func runsActionPlanSteps() async { + let runtime = makeRuntime() + var events: [ActionEvent] = [] + runtime.onAction = { events.append($0) } + runtime.update( + response: """ + $n = 1 + root = Card([b]) + b = Button("Go", Action([@Set($n, $n + 1), @ToAssistant("Bumped", "ctx"), @OpenUrl("https://openui.com")])) + """, isStreaming: false) + let action = runtime.evaluatedRoot()?.props["children"]?.arrayValue?.first?.elementValue? + .props["action"] + + await runtime.triggerAction("Go", action: action) + + #expect(runtime.store.get("$n") == 2) + #expect(events.map(\.type) == ["continue_conversation", "open_url"]) + #expect(events[0].humanFriendlyMessage == "Bumped") + #expect(events[0].params == ["context": "ctx"]) + #expect(events[1].params == ["url": "https://openui.com"]) + + await runtime.triggerAction( + "Reset", action: .actionPlan(ActionPlan(steps: [.reset(targets: ["$n"])]))) + #expect(runtime.store.get("$n") == 1) + } + + @Test func plainAndLegacyActionsContinueTheConversation() async { + let runtime = makeRuntime() + var events: [ActionEvent] = [] + runtime.onAction = { events.append($0) } + runtime.update(response: "root = Card([Text(\"x\")])", isStreaming: false) + + await runtime.triggerAction("Send it") + await runtime.triggerAction( + "Docs", action: ["type": "open_url", "url": "https://openui.com", "params": ["a": 1]]) + + #expect(events[0].type == "continue_conversation") + #expect(events[0].humanFriendlyMessage == "Send it") + #expect(events[1].type == "open_url") + #expect(events[1].params == ["a": 1, "url": "https://openui.com"]) + } + + @Test func formFieldsAreWrappedAndPayloadIsScoped() async { + let runtime = makeRuntime() + var events: [ActionEvent] = [] + runtime.onAction = { events.append($0) } + runtime.update(response: "root = Card([Text(\"x\")])", isStreaming: false) + + runtime.setFieldValue(form: "contact", componentType: "Input", name: "email", value: "a@b.co") + #expect(runtime.fieldValue(form: "contact", name: "email") == "a@b.co") + #expect(runtime.store.get("contact")["email"] == ["value": "a@b.co", "componentType": "Input"]) + + await runtime.triggerAction("Submit", form: "contact") + #expect(events.first?.formName == "contact") + #expect(events.first?.formState?.keys == ["contact"]) + } + + @Test func reactiveBindingWritesState() { + let runtime = makeRuntime() + runtime.update( + response: + "$v = \"start\"\nroot = Card([Input(\"name\", $v), Input(\"upper\", $u = $value + \"!\")])", + isStreaming: false) + let inputs = runtime.evaluatedRoot()?.props["children"]?.arrayValue ?? [] + + let plain = runtime.stateField( + name: "name", binding: inputs[0].elementValue!.props["value"]!, form: nil) + #expect(plain.isReactive) + #expect(plain.value == "start") + plain.setValue("typed") + #expect(runtime.store.get("$v") == "typed") + + let derived = runtime.stateField( + name: "upper", binding: inputs[1].elementValue!.props["value"]!, form: nil) + derived.setValue("hey") + #expect(runtime.store.get("$u") == "hey!") + } + + /// lang-core nests the binding of a reactive input inside `@Each` four levels + /// deep, so typing writes `undefined` into the state. The Swift port keeps the + /// binding intact and writes the typed value. + @Test func reactiveBindingInsideEachWritesState() { + let runtime = makeRuntime("chat") + runtime.update( + response: """ + $items = ["a"] + $v = "start" + root = Card([Form("f", Buttons([Button("ok")]), @Each($items, it, FormControl("L", Input("n", "p", "text", null, $v))))]) + """, isStreaming: false) + let form = runtime.evaluatedRoot()?.props["children"]?.arrayValue?.first?.elementValue + let input = form?.props["fields"]?.arrayValue?.first?.elementValue?.props["input"]? + .elementValue + let binding = input?.props["value"] ?? .undefined + #expect(binding == .reactiveAssign(ReactiveAssign(target: "$v", expr: .stateRef("$value")))) + + runtime.stateField(name: "n", binding: binding, form: "f").setValue("typed") + #expect(runtime.store.get("$v") == "typed") + } + + @Test func stateUpdatesReachTheHostAfterInitialization() { + let runtime = makeRuntime() + var updates = 0 + runtime.onStateUpdate = { _ in updates += 1 } + runtime.update(response: "$n = 1\nroot = Card([Text(\"x\")])", isStreaming: false) + #expect(updates == 0) + runtime.store.set("$n", 2) + #expect(updates == 1) + } + + @Test func emptyResponseClearsTheResult() { + let runtime = makeRuntime() + runtime.update(response: "root = Card([Text(\"x\")])", isStreaming: false) + runtime.update(response: "", isStreaming: false) + #expect(runtime.parseResult == nil) + #expect(runtime.evaluatedRoot() == nil) + } +} diff --git a/packages/swift-lang/Tests/OpenUISwiftUITests/Fixtures/chart-formats.json b/packages/swift-lang/Tests/OpenUISwiftUITests/Fixtures/chart-formats.json new file mode 100644 index 0000000000..2a216c4032 --- /dev/null +++ b/packages/swift-lang/Tests/OpenUISwiftUITests/Fixtures/chart-formats.json @@ -0,0 +1,31 @@ +[ +{"value":0,"tick":"0","tooltip":"0"}, +{"value":1,"tick":"1","tooltip":"1"}, +{"value":-1,"tick":"-1","tooltip":"-1"}, +{"value":7,"tick":"7","tooltip":"7"}, +{"value":0.5,"tick":"0.50","tooltip":"0.5"}, +{"value":0.125,"tick":"0.13","tooltip":"0.125"}, +{"value":1.005,"tick":"1.00","tooltip":"1.005"}, +{"value":1.25,"tick":"1.25","tooltip":"1.25"}, +{"value":12.345,"tick":"12.35","tooltip":"12.345"}, +{"value":-0.04,"tick":"-0.04","tooltip":"-0.04"}, +{"value":0.005,"tick":"0.01","tooltip":"0.005"}, +{"value":999,"tick":"999","tooltip":"999"}, +{"value":999.99,"tick":"999.99","tooltip":"999.99"}, +{"value":1000,"tick":"1.0K","tooltip":"1,000"}, +{"value":1250,"tick":"1.3K","tooltip":"1,250"}, +{"value":9999,"tick":"10.0K","tooltip":"9,999"}, +{"value":10000,"tick":"10K","tooltip":"10,000"}, +{"value":15500,"tick":"16K","tooltip":"15,500"}, +{"value":-2500,"tick":"-2.5K","tooltip":"-2,500"}, +{"value":99999.5,"tick":"100K","tooltip":"99,999.5"}, +{"value":100000,"tick":"100K","tooltip":"100K"}, +{"value":123456,"tick":"123K","tooltip":"123.4K"}, +{"value":999999,"tick":"1000K","tooltip":"999.9K"}, +{"value":1234567,"tick":"1.2M","tooltip":"1.2M"}, +{"value":-7654321,"tick":"-7.7M","tooltip":"-7.6M"}, +{"value":2500000000,"tick":"2.5B","tooltip":"2.5B"}, +{"value":1000000000000,"tick":"1.0T","tooltip":"1T"}, +{"value":15000000000000,"tick":"15T","tooltip":"15T"}, +{"value":1.23456,"tick":"1.23","tooltip":"1.235"} +] diff --git a/packages/swift-lang/Tests/OpenUISwiftUITests/Fixtures/chat-examples.json b/packages/swift-lang/Tests/OpenUISwiftUITests/Fixtures/chat-examples.json new file mode 100644 index 0000000000..35802c93cc --- /dev/null +++ b/packages/swift-lang/Tests/OpenUISwiftUITests/Fixtures/chat-examples.json @@ -0,0 +1,7 @@ +[ +{"name":"chat-example-1","input":"Example 1 — Informational response (the shape for \"tell me about X\" / explainer asks):\n\nroot = Card([header, intro, highlights, statsHeader, statsCards, sectionsBlock, actionBtns], [src1, src2])\nheader = CardHeader(\"Paris\", \"The City of Light · France's Eternal Capital\")\nintro = TextContent(\"Paris is one of the world's most visited cities — a timeless blend of iconic landmarks, world-class cuisine, art, and romance [1]. In 2026 the city is buzzing with new energy: the **Grand Palais** has reopened after a €466M renovation, and you can now **swim in the Seine** for the first time in over a century [2].\")\nhighlights = ContextCardBlock([h1, h2, h3], \"grid\", true, { type: \"continue_conversation\", context: \"Tell me more about this aspect of Paris\" })\nh1 = ContextCardItem(\"art-museums\", \"Art & Museums\", \"143 museums including the Louvre & newly reopened Grand Palais.\", \"gray\", \"https://cdn.britannica.com/03/121003-050-2544BD4E/Interior-Louvre-Museum-Paris.jpg\")\nh2 = ContextCardItem(\"gastronomy\", \"Gastronomy\", \"9,000+ restaurants, 130+ Michelin stars, and the world's best baguettes.\", \"gray\")\nh3 = ContextCardItem(\"fashion-capital\", \"Fashion Capital\", \"Home to Chanel, Dior, Louis Vuitton & Hermès — the global luxury hub.\", \"gray\", \"https://thumbs.dreamstime.com/z/dior-storefront-facade-chanel-store-lvmh-s-french-designer-modehouse-christian-adjacent-to-luxury-fashion-beauty-brand-327161553.jpg\")\nstatsHeader = InlineHeader(\"Paris by the Numbers\", \"Key visitor statistics\")\nstatsCards = OverviewCardBlock([ov1, ov2, ov3], \"grid\", true)\nov1 = OverviewCardItem(\"annual-visitors\", ov1top, ov1metric)\nov1top = IconText(ov1icon, \"neutral\", \"m\", \"Annual Visitors\", \"2024 arrivals\", false, \"vertical\")\nov1icon = Icon(\"users\", \"people\")\nov1metric = MetricIndicatorInline(\"48.7M\", \"tourists\", { direction: \"up\", value: 2.5 })\nov2 = OverviewCardItem(\"museums-monuments\", ov2top, ov2metric)\nov2top = IconText(ov2icon, \"neutral\", \"m\", \"Museums & Monuments\", \"Cultural sites\", false, \"vertical\")\nov2icon = Icon(\"landmark\", \"buildings\")\nov2metric = MetricIndicatorInline(\"2,370+\", \"across the city\")\nov3 = OverviewCardItem(\"tourism-revenue\", ov3top, ov3metric)\nov3top = IconText(ov3icon, \"neutral\", \"m\", \"Tourism Revenue\", \"2024 estimate\", false, \"vertical\")\nov3icon = Icon(\"circle-dollar-sign\", \"finance\")\nov3metric = MetricIndicatorInline(\"€23.4B\", \"generated\")\nsectionsBlock = SectionBlock([secLandmarks, secFood, secWhen], false)\nsecLandmarks = SectionItem(\"landmarks\", \"Iconic Landmarks\", [landmarksCarousel])\nlandmarksCarousel = VisualCardBlock([lm1, lm2, lm3, lm4], \"carousel\", true, { type: \"continue_conversation\", context: \"Tell me more about this Paris landmark\" })\nlm1 = VisualCardItem(lm1body, \"eiffel-tower\", \"https://c8.alamy.com/comp/RYEB13/aerial-view-of-the-eiffel-tower-with-the-park-champ-de-mars-and-the-river-seine-paris-france-RYEB13.jpg\", lm1tag, \"Aerial view of the Eiffel Tower\")\nlm1body = BoldText(\"text\", \"Eiffel Tower\", \"Open daily 9am–midnight\")\nlm1tag = Tag(\"Must-See\", lm1tagIcon, \"sm\", \"info\")\nlm1tagIcon = Icon(\"star\", \"shapes\")\nlm2 = VisualCardItem(lm2body, \"the-louvre\", \"https://images.fineartamerica.com/images/artworkimages/mediumlarge/3/1-exterior-of-musee-du-louvre-museum-paris-france-bernard-jaubert.jpg\", lm2tag, \"Exterior of the Louvre museum\")\nlm2body = BoldText(\"text\", \"The Louvre\", \"World's largest art museum\")\nlm2tag = Tag(\"8.9M visits/yr\", lm2tagIcon, \"sm\", \"success\")\nlm2tagIcon = Icon(\"trending-up\", \"charts\")\nlm3 = VisualCardItem(lm3body, \"notre-dame\", \"https://cdn.britannica.com/29/255529-050-63A22A3C/notre-dame-de-paris-cathedral-paris-france.jpg\", lm3tag, \"Notre-Dame cathedral facade\")\nlm3body = BoldText(\"text\", \"Notre-Dame\", \"Fully restored cathedral\")\nlm3tag = Tag(\"Reopened 2024\", lm3tagIcon, \"sm\", \"warning\")\nlm3tagIcon = Icon(\"hammer\", \"tools\")\nlm4 = VisualCardItem(lm4body, \"versailles\", \"https://cdn.britannica.com/76/130076-050-31128A3D/Latona-Fountain-Balthazard-Marsy-Gaspard-Andre-Le.jpg\", lm4tag, \"Latona Fountain at Versailles\")\nlm4body = BoldText(\"text\", \"Palace of Versailles\", \"Royal gardens & grand halls\")\nlm4tag = Tag(\"Day Trip\", lm4tagIcon, \"sm\", \"neutral\")\nlm4tagIcon = Icon(\"train-front\", \"travel\")\nsecFood = SectionItem(\"food\", \"Food & Drink\", [foodCards])\nfoodCards = ContextCardBlock([f1, f2, f3], \"grid\", true, { type: \"continue_conversation\", context: \"Tell me more about eating and drinking in Paris\" })\nf1 = ContextCardItem(\"bistros\", \"Bistros\", \"Classic French fare — try Les Arlots for confit beef cheeks.\", \"gray\", \"https://everydayparisian.com/wp-content/uploads/2023/01/IMG_0221-768x1024.webp\")\nf2 = ContextCardItem(\"fine-dining\", \"Fine Dining\", \"Septime leads a new wave of creative Michelin-starred cuisine.\", \"gray\", \"https://production-data.worldofmouth.app/images/72b2e6db-97ea-49c4-aa5d-3b7c8121c812.jpg\")\nf3 = ContextCardItem(\"bakeries\", \"Bakeries\", \"Shinya Pain for sourdough. Grand Prix-winning baguettes citywide.\", \"gray\", \"https://www.davidlebovitz.com/wp-content/uploads/2018/09/Le-petit-grain-paris-bakery-boulangerie-patisserie-pastry-shop-8-640x895.jpg\")\nsecWhen = SectionItem(\"when\", \"Best Time to Visit\", [whenTabs])\nwhenTabs = Tabs([tabSpring, tabSummer])\ntabSpring = TabItem(\"spring\", \"Spring\", [springList])\nspringList = ListBlock([sp1, sp2, sp3], \"number\")\nsp1 = ListItem(\"Cherry blossoms at Parc de Sceaux\", \"March – May\", null, null, { type: \"continue_conversation\", context: \"Tell me about cherry blossom season in Paris\" })\nsp2 = ListItem(\"Picnics along the Seine & in city parks\", \"Warm, lively atmosphere\", null, null, { type: \"continue_conversation\", context: \"Where are the best picnic spots in Paris?\" })\nsp3 = ListItem(\"Fewer crowds than summer\", \"Ideal for sightseeing\", null, null, { type: \"continue_conversation\", context: \"How busy is Paris in spring?\" })\ntabSummer = TabItem(\"summer\", \"Summer\", [summerList])\nsummerList = ListBlock([su1, su2, su3], \"number\")\nsu1 = ListItem(\"Swim in the Seine — open July & August 2026\", \"First time in 102 years\", null, null, { type: \"continue_conversation\", context: \"Tell me about swimming in the Seine\" })\nsu2 = ListItem(\"Open-air cinema at Parc de la Villette\", \"Free films under the stars\", null, null, { type: \"continue_conversation\", context: \"Tell me about the open-air cinema at Parc de la Villette\" })\nsu3 = ListItem(\"Bastille Day celebrations on July 14\", \"Fireworks at the Eiffel Tower\", null, null, { type: \"continue_conversation\", context: \"What happens in Paris on Bastille Day?\" })\nactionBtns = Buttons([btn1, btn2, btn3], \"row\")\nbtn1 = Button(\"Plan a Trip to Paris\", { type: \"continue_conversation\", context: \"Help me plan a trip to Paris\" }, \"primary\")\nbtn2 = Button(\"Best Hotels in Paris\", { type: \"continue_conversation\", context: \"Show me the best hotels in Paris\" }, \"secondary\")\nbtn3 = Button(\"Build a Paris Itinerary\", { type: \"continue_conversation\", context: \"Build a 5-day Paris itinerary for me\" }, \"secondary\")\nsrc1 = { title: \"Paris Travel Guide 2025\", sourceName: \"Travel and Tour World\", url: \"https://www.travelandtourworld.com/news/article/from-iconic-landmarks-to-hidden-gems-the-ultimate-paris-travel-guide-for-2025-you-need-to-read-now/\" }\nsrc2 = { title: \"Best Things to Do in Paris 2026\", sourceName: \"Time Out Paris\", url: \"http://www.timeout.fr/paris/en/for-tourists\" }"}, +{"name":"chat-example-2","input":"Example 2 — Data and analytics response (quantitative asks lead with KPIs and charts):\n\nroot = Card([pageHeader, kpiBlock, sections, deepDiveHeader, compositeCards, actionButtons])\npageHeader = CardHeader(\"Analytics Dashboard\", \"April 2026 · Business Performance Overview\")\nkpiBlock = SnippetCardBlock([kpi1, kpi2, kpi3, kpi4], \"grid\", true)\nkpi1 = SnippetCardItem(\"total-revenue\", kpi1lhs, kpi1rhs)\nkpi1lhs = IconText(kpi1icon, \"neutral\", \"m\", \"Total Revenue\", \"Month to date\", false, \"horizontal\")\nkpi1icon = Icon(\"circle-dollar-sign\", \"finance\")\nkpi1rhs = BoldText(\"number\", \"$284,500\", \"+12.4%\", \"metric\")\nkpi2 = SnippetCardItem(\"active-users\", kpi2lhs, kpi2rhs)\nkpi2lhs = IconText(kpi2icon, \"neutral\", \"m\", \"Active Users\", \"Monthly active users\", false, \"horizontal\")\nkpi2icon = Icon(\"users\", \"people\")\nkpi2rhs = BoldText(\"number\", \"47,320\", \"+6.8%\", \"metric\")\nkpi3 = SnippetCardItem(\"conversion-rate\", kpi3lhs, kpi3rhs)\nkpi3lhs = IconText(kpi3icon, \"neutral\", \"m\", \"Conversion Rate\", \"Avg. this month\", false, \"horizontal\")\nkpi3icon = Icon(\"trending-up\", \"charts\")\nkpi3rhs = BoldText(\"number\", \"5.7%\", \"+0.9%\", \"metric\")\nkpi4 = SnippetCardItem(\"churn-rate\", kpi4lhs, kpi4rhs)\nkpi4lhs = IconText(kpi4icon, \"neutral\", \"m\", \"Churn Rate\", \"Monthly customer churn\", false, \"horizontal\")\nkpi4icon = Icon(\"user-minus\", \"people\")\nkpi4rhs = BoldText(\"number\", \"2.1%\", \"-0.4%\", \"metric\")\nsections = SectionBlock([secUsers, secChannels], false)\nsecUsers = SectionItem(\"users\", \"User Activity\", [userTabs])\nuserTabs = Tabs([tabGrowth, tabRetention])\ntabGrowth = TabItem(\"growth\", \"User Growth\", [growthChart])\ngrowthChart = LineChart(months, [dauSeries, mauSeries], \"natural\", \"Month\", \"Users (k)\")\nmonths = [\"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\", \"Jan\", \"Feb\", \"Mar\", \"Apr\"]\ndauSeries = Series(\"DAU\", [5.1, 5.4, 5.8, 6.2, 6.7, 7.1, 7.6, 8.0, 8.4, 8.9, 9.3, 9.8])\nmauSeries = Series(\"MAU\", [28.4, 30.1, 31.8, 33.5, 35.2, 37.1, 38.8, 40.4, 42.1, 43.9, 45.6, 47.3])\ntabRetention = TabItem(\"retention\", \"Cohort Retention\", [retentionChart])\nretentionChart = LineChart(cohortMonths, [cohortJan, cohortFeb, cohortMar], \"natural\", \"Month Since Signup\", \"Retention (%)\")\ncohortMonths = [\"Month 0\", \"Month 1\", \"Month 2\", \"Month 3\", \"Month 4\", \"Month 5\", \"Month 6\"]\ncohortJan = Series(\"Jan Cohort\", [100, 80, 70, 62, 56, 51, 47])\ncohortFeb = Series(\"Feb Cohort\", [100, 83, 73, 65, 59, 54, 50])\ncohortMar = Series(\"Mar Cohort\", [100, 86, 76, 68, 62, 0, 0])\nsecChannels = SectionItem(\"channels\", \"Acquisition Channels\", [channelTabs])\nchannelTabs = Tabs([tabChannelBar, tabChannelTable])\ntabChannelBar = TabItem(\"channel-bar\", \"Revenue by Channel\", [channelBarChart])\nchannelBarChart = HorizontalBarChart(channels, [channelSeries], \"grouped\", \"Revenue ($k)\", \"Channel\")\nchannels = [\"Organic Search\", \"Paid Ads\", \"Referral Program\", \"Partner Network\", \"Direct / Brand\", \"Email Campaigns\"]\nchannelSeries = Series(\"Revenue ($k)\", [82.4, 68.1, 54.6, 38.2, 26.8, 14.4])\ntabChannelTable = TabItem(\"channel-table\", \"Full Breakdown\", [channelTable])\nchannelTable = Table([colChannel, colLeads, colConv, colCac, colRevenue, colStatus])\ncolChannel = Col(\"Channel\", channels, \"string\")\ncolLeads = Col(\"Leads\", [4210, 3180, 2050, 1120, 1290, 820], \"number\")\ncolConv = Col(\"Conv. Rate\", [\"6.4%\", \"5.1%\", \"9.4%\", \"11.8%\", \"5.8%\", \"8.1%\"], \"string\")\ncolCac = Col(\"CAC ($)\", [188, 395, 132, 109, 218, 154], \"number\")\ncolRevenue = Col(\"Revenue ($k)\", [82.4, 68.1, 54.6, 38.2, 26.8, 14.4], \"number\")\ncolStatus = Col(\"Status\", [\"Scaling\", \"Optimizing\", \"Scaling\", \"Growing\", \"Stable\", \"Stable\"], \"string\")\ndeepDiveHeader = InlineHeader(\"Support Health & Marketing ROI\", \"Click a card to explore deeper insights\")\ncompositeCards = CompositeCardBlock([cardSupport, cardMarketing], \"grid\", true, { type: \"continue_conversation\", context: \"Explore this deep-dive area in more detail\" })\ncardSupport = CompositeCardItem(\"support-health\", supportHeader, [supportMetric, supportChart, supportList])\nsupportHeader = IconText(supportIcon, \"neutral\", \"m\", \"Support Health\", \"Ticket volume & resolution trends\", false, \"horizontal\")\nsupportIcon = Icon(\"headphones\", \"communication\")\nsupportMetric = MetricIndicatorInline(\"94.2% CSAT\", \"Customer Satisfaction Score\", { direction: \"up\", value: 2.1 })\nsupportChart = BarChart([\"Jan\", \"Feb\", \"Mar\", \"Apr\"], [openSeries, resolvedSeries], \"grouped\", \"Month\", \"Tickets\")\nopenSeries = Series(\"Opened\", [341, 327, 308, 289])\nresolvedSeries = Series(\"Resolved\", [319, 318, 301, 283])\nsupportList = EntityList([sup1, sup2, sup3], \"small\")\nsup1 = { left: \"Avg. Resolution Time\", right: \"4h 12m\", rightVariant: \"text\" }\nsup2 = { left: \"First Contact Resolution\", right: \"78.4%\", rightVariant: \"number\" }\nsup3 = { left: \"Escalation Rate\", right: \"6.2%\", rightVariant: \"number\" }\ncardMarketing = CompositeCardItem(\"marketing-roi\", marketingHeader, [marketingMetric, marketingChart, marketingList])\nmarketingHeader = IconText(marketingIcon, \"neutral\", \"m\", \"Marketing ROI\", \"Spend vs. pipeline generated\", false, \"horizontal\")\nmarketingIcon = Icon(\"megaphone\", \"communication\")\nmarketingMetric = MetricIndicatorInline(\"3.8x Blended ROI\", \"Return on Ad Spend\", { direction: \"up\", value: 0.4 })\nmarketingChart = AreaChart([\"Jan\", \"Feb\", \"Mar\", \"Apr\"], [spendSeries, pipelineSeries], \"natural\", \"Month\", \"Value ($k)\")\nspendSeries = Series(\"Ad Spend\", [44.0, 46.8, 49.2, 51.6])\npipelineSeries = Series(\"Pipeline Generated\", [158.2, 171.5, 184.9, 196.1])\nmarketingList = EntityList([mkt1, mkt2, mkt3], \"small\")\nmkt1 = { left: \"Total Ad Spend (Apr)\", right: \"$51,600\", rightVariant: \"number\" }\nmkt2 = { left: \"Cost per Lead\", right: \"$48.20\", rightVariant: \"number\" }\nmkt3 = { left: \"Top Channel\", right: \"Organic Search\", rightVariant: \"text\" }\nactionButtons = Buttons([btnChannels, btnForecast], \"row\")\nbtnChannels = Button(\"Break Down by Channel\", { type: \"continue_conversation\", context: \"Break down this month's revenue by acquisition channel\" }, \"primary\")\nbtnForecast = Button(\"Forecast Next Quarter\", { type: \"continue_conversation\", context: \"Forecast revenue and active users for next quarter\" }, \"secondary\")"}, +{"name":"chat-example-3","input":"Example 3 — Form response (help-me-choose/plan asks: inspiration cards FIRST, then a compact form). The inspiration-card images below came from an image tool call — include such cards ONLY when you have real URLs; with no real URLs, start directly with the form:\n\nroot = Card([header, intro, inspoCards, form])\nheader = CardHeader(\"Plan Your Trip\", \"A few ideas to spark inspiration — then tell me your preferences\")\nintro = TextContent(\"Tap a style to jump straight in, or fill the quick form below for a tailored plan.\")\ninspoCards = VisualCardBlock([insp1, insp2], \"grid\", true, { type: \"continue_conversation\", context: \"Plan me a trip in this style with destinations, itinerary, and costs\" })\ninsp1 = VisualCardItem(insp1body, \"inspo-beach\", \"https://www.travelandleisure.com/thmb/cuAB7XMQ6s_Gji-no956KNopT7M=/1500x0/filters:no_upscale():max_bytes(150000):strip_icc()/TAL-maldives-TROPVACAY0325-1b5047d5931d4850b47d082966f9f563.jpg\", insp1tag, \"Maldives overwater villas\")\ninsp1body = BoldText(\"text\", \"Tropical Beach Escape\", \"Overwater villas & snorkeling\")\ninsp1tag = Tag(\"Relaxation\", insp1icon, \"sm\", \"info\")\ninsp1icon = Icon(\"umbrella\", \"travel\")\ninsp2 = VisualCardItem(insp2body, \"inspo-mountain\", \"https://www.salkantaytrekmachu.com/img/adventure-travel-peru-032.jpg\", insp2tag, \"Trekking in the Peruvian Andes\")\ninsp2body = BoldText(\"text\", \"Mountain Adventure\", \"Trekking & epic views\")\ninsp2tag = Tag(\"Active\", insp2icon, \"sm\", \"warning\")\ninsp2icon = Icon(\"mountain\", \"nature\")\nform = Form(\"trip-planner\", formButtons, [fcDestination, fcDates, fcTravellers, fcTripType, fcBudget, fcStay, fcNotes])\nformButtons = Buttons([btnSubmit, btnSkip], \"row\")\nbtnSubmit = Button(\"Plan My Trip\", { type: \"continue_conversation\", context: \"Help me plan a trip based on my form submission\" }, \"primary\")\nbtnSkip = Button(\"Just Surprise Me\", { type: \"continue_conversation\", context: \"Skip the form and surprise me with a destination and itinerary\" }, \"secondary\")\nfcDestination = FormControl(\"Destination\", destinationInput, \"Leave blank if you'd like suggestions\")\ndestinationInput = Input(\"destination\", \"e.g. Tokyo, Japan — or leave blank\", \"text\")\nfcDates = FormControl(\"Travel Dates\", datesPicker, \"Select your start and end dates\")\ndatesPicker = DatePicker(\"travel-dates\", \"range\", { required: true })\nfcTravellers = FormControl(\"Number of Travellers\", travellersSelect, \"How many people are travelling?\")\ntravellersSelect = Select(\"num-travellers\", [tr1, tr2, tr3, tr4], \"Select number of travellers\", { required: true })\ntr1 = SelectItem(\"1\", \"1 — Solo\")\ntr2 = SelectItem(\"2\", \"2 — Couple\")\ntr3 = SelectItem(\"3-4\", \"3–4 People\")\ntr4 = SelectItem(\"5+\", \"5+ People\")\nfcTripType = FormControl(\"Trip Type\", tripTypeCards, \"What kind of trip are you planning?\")\ntripTypeCards = OptionCards(\"trip-type\", \"single\", [oc1, oc2, oc3, oc4], { required: true })\noc1 = OptionCard(\"adventure\", \"Adventure\", \"Hiking, trekking & outdoor activities\", oc1icon)\noc1icon = Icon(\"mountain\", \"nature\")\noc2 = OptionCard(\"beach\", \"Beach & Relaxation\", \"Sun, sand & leisure\", oc2icon)\noc2icon = Icon(\"waves\", \"nature\")\noc3 = OptionCard(\"cultural\", \"Cultural & Historical\", \"Museums, heritage & local experiences\", oc3icon)\noc3icon = Icon(\"landmark\", \"buildings\")\noc4 = OptionCard(\"city\", \"City Break\", \"Urban exploration & nightlife\", oc4icon)\noc4icon = Icon(\"building-2\", \"buildings\")\nfcBudget = FormControl(\"Estimated Budget (per person)\", budgetSlider, \"Drag to set your per-person budget in USD\")\nbudgetSlider = Slider(\"budget\", \"discrete\", 500, 10000, 500, [2500], \"Budget (USD)\")\nfcStay = FormControl(\"Accommodation Preference\", stayChips, \"Select all that apply\")\nstayChips = Chips(\"accommodation\", \"multiple\", [ch1, ch2, ch3, ch4], {}, [\"hotel\"])\nch1 = ChipItem(\"hotel\", \"Hotel\", ch1icon)\nch1icon = Icon(\"building\", \"buildings\")\nch2 = ChipItem(\"resort\", \"Resort\", ch2icon)\nch2icon = Icon(\"umbrella\", \"travel\")\nch3 = ChipItem(\"airbnb\", \"Vacation Rental\", ch3icon)\nch3icon = Icon(\"home\", \"home\")\nch4 = ChipItem(\"camping\", \"Camping / Glamping\", ch4icon)\nch4icon = Icon(\"tent\", \"nature\")\nfcNotes = FormControl(\"Special Requests or Notes\", notesArea, \"Any dietary needs, accessibility requirements, or specific interests?\")\nnotesArea = TextArea(\"special-requests\", \"e.g. vegetarian meals, wheelchair access, honeymoon surprise...\", 4)"}, +{"name":"chat-example-4","input":"Example 4 — Recommendations with images and links (same pattern for products, movies, restaurants, articles):\n\nroot = Card([header, intro, posters, linksHeader, trackCards], [src1, src2])\nheader = CardHeader(\"Retro-Energetic Indian Tracks\", \"New Hindi releases with 80s/90s dance DNA\")\nintro = TextContent(\"These recent tracks channel retro synths and 90s party percussion [1][2]. Tap a poster to explore a track, or use its button to watch the exact official video.\")\nposters = VisualCardBlock([v1, v2], \"grid\", true, { type: \"continue_conversation\", context: \"Tell me more about this track and its retro influences\" })\nv1 = VisualCardItem(v1body, \"tamma\", \"https://i.ytimg.com/vi/PQHeOb0Q9oo/hq720.jpg\", v1tag, \"Tamma Tamma video poster\")\nv1body = BoldText(\"text\", \"Tamma Tamma\", \"Dhurandhar · Ranveer Singh\")\nv1tag = Tag(\"80s Electro-Disco\", v1icon, \"sm\", \"success\")\nv1icon = Icon(\"disc-3\", \"multimedia\")\nv2 = VisualCardItem(v2body, \"sajan\", \"https://i.ytimg.com/vi/mLz-qBiRnl8/hq720.jpg\", v2tag, \"Sajan Re video poster\")\nv2body = BoldText(\"text\", \"Sajan Re\", \"Nora Fatehi · Badshah\")\nv2tag = Tag(\"Synthwave Electro\", v2icon, \"sm\", \"info\")\nv2icon = Icon(\"music\", \"multimedia\")\nlinksHeader = InlineHeader(\"Watch the Exact Videos\", \"Each button opens that track's official video\")\ntrackCards = CompositeCardBlock([t1, t2], \"grid\", true)\nt1 = CompositeCardItem(\"t-tamma\", t1h, [t1body, t1tags], { button: t1btn })\nt1h = IconText(t1icon, \"neutral\", \"m\", \"Tamma Tamma\", \"Bappi Lahiri classic, electro-funk rework\", true, \"horizontal\")\nt1icon = Icon(\"disc-3\", \"multimedia\")\nt1body = Text(\"text\", \"Heavy analog synths and brass over a fast disco groove.\", \"Retro Electro-Disco\")\nt1tags = TagBlock([\"2026 Film Hit\", \"Peak Party\"])\nt1btn = Button(\"Watch on YouTube\", { type: \"open_url\", url: \"https://www.youtube.com/watch?v=PQHeOb0Q9oo\" }, \"primary\")\nt2 = CompositeCardItem(\"t-sajan\", t2h, [t2body, t2tags], { button: t2btn })\nt2h = IconText(t2icon, \"neutral\", \"m\", \"Sajan Re\", \"Nora Fatehi · Badshah · Sanjoy\", true, \"horizontal\")\nt2icon = Icon(\"music\", \"multimedia\")\nt2body = Text(\"text\", \"Synthwave melody hooks over a modern club beat.\", \"Retro-Fusion Electro\")\nt2tags = TagBlock([\"Club Anthem\", \"Nora x Badshah\"])\nt2btn = Button(\"Watch on YouTube\", { type: \"open_url\", url: \"https://www.youtube.com/watch?v=COUSCnFazzc\" }, \"primary\")\nsrc1 = { title: \"Tamma Tamma (Full Video) - Dhurandhar\", sourceName: \"YouTube (T-Series)\", url: \"https://www.youtube.com/watch?v=PQHeOb0Q9oo\" }\nsrc2 = { title: \"Sajan Re (Official Music Video)\", sourceName: \"YouTube (T-Series)\", url: \"https://www.youtube.com/watch?v=COUSCnFazzc\" }"}, +{"name":"chat-example-5","input":"Example 5 — Table with follow-ups:\n\nroot = Card([title, tbl, followUps])\ntitle = TextContent(\"Top Languages\", \"large-heavy\")\ntbl = Table([Col(\"Language\", langs), Col(\"Users (M)\", users), Col(\"Year\", years)])\nlangs = [\"Python\", \"JavaScript\", \"Java\"]\nusers = [15.7, 14.2, 12.1]\nyears = [1991, 1995, 1995]\nfollowUps = FollowUpBlock([fu1, fu2])\nfu1 = FollowUpItem(\"Tell me more about Python\")\nfu2 = FollowUpItem(\"Show me a JavaScript comparison\")"} +] diff --git a/packages/swift-lang/Tests/OpenUISwiftUITests/Fixtures/openui-examples.json b/packages/swift-lang/Tests/OpenUISwiftUITests/Fixtures/openui-examples.json new file mode 100644 index 0000000000..a6b62cfd52 --- /dev/null +++ b/packages/swift-lang/Tests/OpenUISwiftUITests/Fixtures/openui-examples.json @@ -0,0 +1,8 @@ +[ +{"name":"openui-example-1","input":"Example 1 — Table (column-oriented):\n\nroot = Stack([title, tbl])\ntitle = TextContent(\"Top Languages\", \"large-heavy\")\ntbl = Table([Col(\"Language\", langs), Col(\"Users (M)\", users), Col(\"Year\", years)])\nlangs = [\"Python\", \"JavaScript\", \"Java\", \"TypeScript\", \"Go\"]\nusers = [15.7, 14.2, 12.1, 8.5, 5.2]\nyears = [1991, 1995, 1995, 2012, 2009]"}, +{"name":"openui-example-2","input":"Example 2 — Bar chart:\n\nroot = Stack([title, chart])\ntitle = TextContent(\"Q4 Revenue\", \"large-heavy\")\nchart = BarChart(labels, [s1, s2], \"grouped\")\nlabels = [\"Oct\", \"Nov\", \"Dec\"]\ns1 = Series(\"Product A\", [120, 150, 180])\ns2 = Series(\"Product B\", [90, 110, 140])"}, +{"name":"openui-example-3","input":"Example 3 — Form with validation:\n\nroot = Stack([title, form])\ntitle = TextContent(\"Contact Us\", \"large-heavy\")\nform = Form(\"contact\", btns, [nameField, emailField, countryField, msgField])\nnameField = FormControl(\"Name\", Input(\"name\", \"Your name\", \"text\", { required: true, minLength: 2 }))\nemailField = FormControl(\"Email\", Input(\"email\", \"you@example.com\", \"email\", { required: true, email: true }))\ncountryField = FormControl(\"Country\", Select(\"country\", countryOpts, \"Select...\", { required: true }))\nmsgField = FormControl(\"Message\", TextArea(\"message\", \"Tell us more...\", 4, { required: true, minLength: 10 }))\ncountryOpts = [SelectItem(\"us\", \"United States\"), SelectItem(\"uk\", \"United Kingdom\"), SelectItem(\"de\", \"Germany\")]\nbtns = Buttons([Button(\"Submit\", Action([@ToAssistant(\"Submit\")]), \"primary\"), Button(\"Cancel\", Action([@ToAssistant(\"Cancel\")]), \"secondary\")])"}, +{"name":"openui-example-4","input":"Example 4 — Tabs with mixed content:\n\nroot = Stack([title, tabs])\ntitle = TextContent(\"React vs Vue\", \"large-heavy\")\ntabs = Tabs([tabReact, tabVue])\ntabReact = TabItem(\"react\", \"React\", reactContent)\ntabVue = TabItem(\"vue\", \"Vue\", vueContent)\nreactContent = [TextContent(\"React is a library by Meta for building UIs.\"), Callout(\"info\", \"Note\", \"React uses JSX syntax.\")]\nvueContent = [TextContent(\"Vue is a progressive framework by Evan You.\"), Callout(\"success\", \"Tip\", \"Vue has a gentle learning curve.\")]"}, +{"name":"openui-example-5","input":"Example 5 — KPI and product cards:\n\nroot = Stack([kpiHeader, kpiBlock, productsHeader, productCards])\nkpiHeader = InlineHeader(\"This Month\", \"Key account metrics\")\nkpiBlock = SnippetCardBlock([kpi1, kpi2, kpi3])\nkpi1 = SnippetCardItem(\"revenue\", kpi1lhs, kpi1rhs)\nkpi1lhs = IconText(kpi1icon, \"neutral\", \"m\", \"Revenue\", \"Month to date\", false, \"horizontal\")\nkpi1icon = Icon(\"circle-dollar-sign\", \"finance\")\nkpi1rhs = BoldText(\"number\", \"$48,200\", \"+8.1%\", \"metric\")\nkpi2 = SnippetCardItem(\"active-users\", kpi2lhs, kpi2rhs)\nkpi2lhs = IconText(kpi2icon, \"neutral\", \"m\", \"Active Users\", \"This month\", false, \"horizontal\")\nkpi2icon = Icon(\"users\", \"people\")\nkpi2rhs = BoldText(\"number\", \"1,204\", \"+3.4%\", \"metric\")\nkpi3 = SnippetCardItem(\"churn\", kpi3lhs, kpi3rhs)\nkpi3lhs = IconText(kpi3icon, \"neutral\", \"m\", \"Churn\", \"This month\", false, \"horizontal\")\nkpi3icon = Icon(\"user-minus\", \"people\")\nkpi3rhs = BoldText(\"number\", \"1.8%\", \"-0.3%\", \"metric\")\nproductsHeader = InlineHeader(\"Top Products\", \"By units sold\")\nproductCards = CompositeCardBlock([p1, p2])\np1 = CompositeCardItem(\"widget-pro\", p1header, [p1body], p1footer)\np1header = IconText(p1icon, \"neutral\", \"m\", \"Widget Pro\", \"Best seller\", true, \"horizontal\")\np1icon = Icon(\"package\", \"shopping\")\np1body = MetricIndicatorInline(\"2,410 units\", \"Sold this month\", { direction: \"up\", value: 12 })\np1footer = { price: BoldText(\"text\", \"$29.00\"), button: p1btn }\np1btn = Button(\"View Details\", Action([@ToAssistant(\"Show details for Widget Pro\")]), \"secondary\")\np2 = CompositeCardItem(\"widget-mini\", p2header, [p2body], p2footer)\np2header = IconText(p2icon, \"neutral\", \"m\", \"Widget Mini\", \"New arrival\", true, \"horizontal\")\np2icon = Icon(\"box\", \"shopping\")\np2body = MetricIndicatorInline(\"980 units\", \"Sold this month\", { direction: \"up\", value: 4 })\np2footer = { price: BoldText(\"text\", \"$14.00\"), button: p2btn }\np2btn = Button(\"View Details\", Action([@ToAssistant(\"Show details for Widget Mini\")]), \"secondary\")"}, +{"name":"openui-example-6","input":"Example 6 — Editable table and a selection form:\n\nroot = Stack([tableHeader, editTable, formHeader, prefsForm])\ntableHeader = InlineHeader(\"Team Roster\", \"Click a cell to edit\")\neditTable = EditableTable(\"roster\", [colName, colRole, colStart], [row1, row2])\ncolName = { type: \"text\", key: \"name\", header: \"Name\" }\ncolRole = { type: \"select\", key: \"role\", header: \"Role\", options: [roleEng, roleDesign] }\nroleEng = { value: \"eng\", label: \"Engineering\" }\nroleDesign = { value: \"design\", label: \"Design\" }\ncolStart = { type: \"date-single\", key: \"start\", header: \"Start Date\" }\nrow1 = { id: \"1\", values: [\"Alex Kim\", \"eng\", \"2024-01-15\"] }\nrow2 = { id: \"2\", values: [\"Jamie Lee\", \"design\", \"2024-03-02\"] }\nformHeader = InlineHeader(\"Preferences\", \"Tell us how you like to work\")\nprefsForm = Form(\"prefs\", formButtons, [fc1, fc2])\nfc1 = FormControl(\"Working Style\", styleCards, \"Pick the one that fits best\")\nstyleCards = OptionCards(\"style\", \"single\", [style1, style2])\nstyle1 = OptionCard(\"focused\", \"Deep Focus\", \"Long uninterrupted blocks\", style1icon)\nstyle1icon = Icon(\"target\", \"tools\")\nstyle2 = OptionCard(\"collab\", \"Collaborative\", \"Frequent pairing and syncs\", style2icon)\nstyle2icon = Icon(\"users\", \"people\")\nfc2 = FormControl(\"Tools\", toolChips, \"Select all that apply\")\ntoolChips = Chips(\"tools\", \"multiple\", [tool1, tool2, tool3])\ntool1 = ChipItem(\"figma\", \"Figma\", tool1icon)\ntool1icon = Icon(\"figma\", \"design\")\ntool2 = ChipItem(\"slack\", \"Slack\", tool2icon)\ntool2icon = Icon(\"message-square\", \"communication\")\ntool3 = ChipItem(\"notion\", \"Notion\", tool3icon)\ntool3icon = Icon(\"notebook\", \"text\")\nformButtons = Buttons([Button(\"Save\", Action([@ToAssistant(\"Save preferences\")]), \"primary\")])"} +] diff --git a/packages/swift-lang/Tests/OpenUISwiftUITests/RendererTests.swift b/packages/swift-lang/Tests/OpenUISwiftUITests/RendererTests.swift new file mode 100644 index 0000000000..d3f84eb971 --- /dev/null +++ b/packages/swift-lang/Tests/OpenUISwiftUITests/RendererTests.swift @@ -0,0 +1,1053 @@ +import Foundation +import SwiftUI +import Testing + +@testable import OpenUISwiftUI + +#if canImport(AppKit) + import AppKit +#endif + +@MainActor +@Suite struct ChatLibraryTests { + @Test func everyComponentHasAView() { + for schema in ChatComponents.all { + #expect( + OpenUIChatLibrary.views[schema.name] != nil + || OpenUIChatLibrary.dataOnly.contains(schema.name), + "\(schema.name) has no view") + } + #expect(OpenUIChatLibrary.library.rootName == "Card") + #expect(OpenUIChatLibrary.library.components.count == ChatComponents.all.count) + } +} + +/// A card's text block counts as content only when it has text: one whose +/// values came out empty doesn't get an empty panel over the photo. +@Suite struct CardTextContentTests { + func block(_ type: String, _ props: OpenUIObject) -> OpenUIValue { + .element(ElementNode(typeName: type, props: props, partial: false)) + } + + @Test func emptyTextBlocksHaveNoContent() { + #expect(!textBlockHasContent(block("BoldText", ["value": "", "subtext": ""]))) + #expect(!textBlockHasContent(block("Text", ["value": .null]))) + #expect(!textBlockHasContent(.null)) + } + + @Test func textOrOtherComponentsAreContent() { + #expect(textBlockHasContent(block("BoldText", ["value": "", "subtext": "Temple"]))) + #expect(textBlockHasContent(block("Text", ["value": 0]))) + #expect(textBlockHasContent(block("TagBlock", ["tags": []]))) + } +} + +@Suite struct CardActionTests { + let item: OpenUIObject = ["itemIndex": 1, "itemId": "a", "itemTitle": .undefined] + + @Test func noActionContinuesWithItemContext() { + #expect( + withItemContext(.null, item) + == ["type": "continue_conversation", "params": ["itemIndex": 1, "itemId": "a"]]) + } + + @Test func actionPlanAppendsSelectedItemToToAssistant() { + let plan = OpenUIValue.actionPlan( + ActionPlan(steps: [ + .continueConversation(message: "Go", context: "ctx"), + .continueConversation(message: "Bare", context: nil), + .openUrl(url: "https://openui.com"), + ])) + guard case .actionPlan(let merged) = withItemContext(plan, item) else { + Issue.record("expected an action plan") + return + } + let suffix = #"Selected item: {"itemIndex":1,"itemId":"a"}"# + #expect( + merged.steps == [ + .continueConversation(message: "Go", context: "ctx\n\(suffix)"), + .continueConversation(message: "Bare", context: suffix), + .openUrl(url: "https://openui.com"), + ]) + #expect(withItemContext(plan, [:]) == plan) + } + + @Test func legacyActionMergesParams() { + let legacy: OpenUIValue = [ + "type": "continue_conversation", "context": "Explore", "params": ["x": 1], + ] + #expect( + withItemContext(legacy, item) + == [ + "type": "continue_conversation", + "params": ["x": 1, "context": "Explore", "itemIndex": 1, "itemId": "a"], + ]) + } +} + +@Suite struct IconTests { + @Test func mapsLucideNamesWithFallbacks() { + #expect(LucideSymbols.systemName(for: "users", category: nil) == "person.2") + #expect( + LucideSymbols.systemName(for: "triangle-alert", category: nil) == "exclamationmark.triangle") + #expect(LucideSymbols.systemName(for: "no-such-icon", category: "finance") == "dollarsign") + #expect( + LucideSymbols.systemName(for: "no-such-icon", category: nil) + == LucideSymbols.symbols[LucideSymbols.defaultFallback]) + } + + @Test func categoryFallbacksResolve() { + for (category, name) in LucideSymbols.categoryFallbacks { + #expect(LucideSymbols.symbols[name] != nil, "\(category) → \(name)") + } + } + + #if canImport(AppKit) + @Test func everyMappedSymbolExists() { + for (name, symbol) in LucideSymbols.symbols { + #expect( + NSImage(systemSymbolName: symbol, accessibilityDescription: nil) != nil, + "\(name) → \(symbol)") + } + } + #endif +} + +@Suite struct NodeIdentityTests { + @Test func prefersStatementIdsAndStaysUnique() { + let element = { (id: String?) -> OpenUIValue in + .element(ElementNode(statementId: id, typeName: "TextContent", props: [:], partial: false)) + } + let items = NodeItem.list([element("a"), .null, element(nil), element("a"), "text"]) + #expect(items.map(\.id) == ["a", "#2", "a~1", "#4"]) + } +} + +@MainActor +@Suite struct OpenUIContextTests { + @Test func updatesAndRoutesActions() async { + let context = OpenUIContext(library: OpenUIChatLibrary.library) + var events: [ActionEvent] = [] + context.onAction = { events.append($0) } + let start = context.revision + + context.update( + response: "root = Card([FollowUpBlock([FollowUpItem(\"More\")])])", isStreaming: false) + #expect(context.revision > start) + #expect(context.root?.typeName == "Card") + + await context.runtime.triggerAction("More") + #expect(events.map(\.humanFriendlyMessage) == ["More"]) + + let before = context.revision + context.runtime.store.set("$x", 1) + #expect(context.revision > before) + } + + @Test func seedsDefaultsOnlyAfterStreaming() { + let context = OpenUIContext(library: OpenUIChatLibrary.library) + context.update(response: "root = Card([])", isStreaming: true) + context.setDefaultValue(form: "f", componentType: "Chips", name: "c", value: ["a"]) + #expect(context.fieldValue(form: "f", name: "c") == .undefined) + + context.update(response: "root = Card([])", isStreaming: false) + context.setDefaultValue(form: "f", componentType: "Chips", name: "c", value: ["a"]) + #expect(context.fieldValue(form: "f", name: "c") == ["a"]) + } +} + +#if canImport(AppKit) + /// Renders each chat library example end to end, offscreen, and checks it + /// produced a real layout (not an empty or collapsed view). + @MainActor + @Suite struct EndToEndRenderTests { + nonisolated static let examples: [(name: String, input: String)] = { + let url = Bundle.module.url( + forResource: "chat-examples", withExtension: "json", subdirectory: "Fixtures")! + let json = try! JSON.parse(String(contentsOf: url, encoding: .utf8)) + return (json.arrayValue ?? []).map { ($0["name"].stringValue!, $0["input"].stringValue!) } + }() + + @Test(arguments: examples.map(\.name)) + func rendersChatExample(_ name: String) { + let input = Self.examples.first { $0.name == name }!.input + let host = NSHostingView( + rootView: OpenUIRenderer(response: input, library: OpenUIChatLibrary.library) + .frame(width: 420)) + let window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 420, height: 600), styleMask: [.borderless], + backing: .buffered, defer: false) + window.contentView = host + RunLoop.main.run(until: Date().addingTimeInterval(0.3)) + // The smallest example (a short table) is about 195pt; empty or collapsed + // renders are far below this. + #expect(host.fittingSize.height > 120, "\(name) rendered \(host.fittingSize)") + } + + nonisolated static let openUIExamples: [(name: String, input: String)] = { + let url = Bundle.module.url( + forResource: "openui-examples", withExtension: "json", subdirectory: "Fixtures")! + let json = try! JSON.parse(String(contentsOf: url, encoding: .utf8)) + return (json.arrayValue ?? []).map { ($0["name"].stringValue!, $0["input"].stringValue!) } + }() + + /// The same for react-ui's general library, `openuiLibrary`, whose + /// examples start from a Stack. + @Test(arguments: openUIExamples.map(\.name)) + func rendersOpenUIExample(_ name: String) { + let input = Self.openUIExamples.first { $0.name == name }!.input + var errors: [OpenUIError] = [] + let host = NSHostingView( + rootView: OpenUIRenderer( + response: input, library: OpenUILibrary.library, onError: { errors = $0 } + ) + .frame(width: 420)) + let window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 420, height: 600), styleMask: [.borderless], + backing: .buffered, defer: false) + window.contentView = host + RunLoop.main.run(until: Date().addingTimeInterval(0.3)) + #expect(host.fittingSize.height > 100, "\(name) rendered \(host.fittingSize)") + // Only the example's prose heading may fail to parse. + #expect(errors.allSatisfy { $0.jsonRepresentation["source"] == "parser" }, "\(errors)") + } + + /// Numbers a response can hold but a layout can't use (1e999, 0/0, a + /// billion slider steps) render without crashing or hanging. + @Test(arguments: [ + #"TextArea("n", "p", 1e999)"#, + #"Slider("s", "discrete", 0, 1e999, 1)"#, + #"Slider("s", "discrete", 0, 1000000, 0.001)"#, + #"Slider("s", "continuous", 0/0, 10, 1)"#, + #"Slider("s", "continuous", 0, 100, 1, [10, 1e999])"#, + // Mid-stream, `max` can be cut short below `min` ("8000" arrives as "800"). + #"Slider("s", "discrete", 2000, 800)"#, + #"Slider("s", "continuous", 5, 5)"#, + #"Slider("s", "discrete", 0, 10, 50)"#, + #"BarChart(["A"], [Series("S", [1])], "grouped", "x", "y", 1e999)"#, + #"RadarChart(["A", "B", "C"], [Series("S", [1, 1e999, 3])])"#, + #"ScatterChart([ScatterSeries("S", [Point(1, 2), Point(0/0, 1)])])"#, + ]) + func rendersNumbersALayoutCantUse(_ component: String) { + let host = NSHostingView( + rootView: OpenUIRenderer( + response: "root = Card([a])\na = \(component)", library: OpenUIChatLibrary.library + ) + .frame(width: 420)) + let window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 420, height: 600), styleMask: [.borderless], + backing: .buffered, defer: false) + window.contentView = host + RunLoop.main.run(until: Date().addingTimeInterval(0.3)) + #expect(host.fittingSize.height > 20, "\(component) rendered \(host.fittingSize)") + } + + /// Offered exactly its ideal height (a fixed frame, a self-sizing cell), + /// a response lays out at that height. A VStack split the height by + /// flexibility instead, cutting wrapping text short next to a row with a + /// Spacer and leaving the rest of the height empty. + @Test func fillsExactlyItsIdealHeight() { + final class Box { var height: CGFloat = 0 } + let response = """ + root = Card([intro, tip]) + intro = TextContent("That is **Bangalore Palace** in Bengaluru, Karnataka! Built in 1878, its Tudor-style architecture was inspired by Windsor Castle, with fortified towers, battlements and lush gardens.") + tip = Callout("info", "Good to know", "Sprawling gardens used for cultural exhibitions, concerts, flower shows and public events across the year.") + """ + let measured = NSHostingView( + rootView: OpenUIRenderer(response: response, library: OpenUIChatLibrary.library) + .frame(width: 340)) + RunLoop.main.run(until: Date().addingTimeInterval(0.2)) + let ideal = measured.fittingSize.height + let box = Box() + let host = NSHostingView( + rootView: OpenUIRenderer(response: response, library: OpenUIChatLibrary.library) + .onGeometryChange(for: CGFloat.self) { + $0.size.height + } action: { + box.height = $0 + } + .frame(width: 340, height: ideal)) + let window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 340, height: ideal), styleMask: [.borderless], + backing: .buffered, defer: false) + window.contentView = host + RunLoop.main.run(until: Date().addingTimeInterval(0.3)) + #expect(ideal > 100) + #expect(abs(box.height - ideal) < 1, "laid out \(box.height) of \(ideal)") + } + + /// A closed Modal takes no room in a Stack, gap included, as in react-ui + /// where it renders nothing. + @Test func closedModalTakesNoRoom() { + func height(_ input: String) -> CGFloat { + let host = NSHostingView( + rootView: OpenUIRenderer(response: input, library: OpenUILibrary.library) + .frame(width: 420)) + RunLoop.main.run(until: Date().addingTimeInterval(0.2)) + return host.fittingSize.height + } + let alone = height(#"root = Stack([TextContent("Profile"), TextContent("Name")])"#) + let withModal = height( + """ + $open = false + root = Stack([TextContent("Profile"), modal, TextContent("Name")]) + modal = Modal("Edit", $open, [TextContent("Hi")]) + """) + #expect(alone > 0) + #expect(withModal == alone) + } + + /// openuiLibrary's Modal opens from its `open` binding, and closing the + /// sheet (its X, Escape, a swipe) writes false back. + @Test func modalFollowsItsOpenBinding() { + final class Box { var state = OpenUIObject() } + let box = Box() + let input = """ + $open = true + root = Stack([title, modal]) + title = TextContent("Profile") + modal = Modal("Edit profile", $open, [TextContent("Change your name")]) + """ + let host = NSHostingView( + rootView: OpenUIRenderer( + response: input, library: OpenUILibrary.library, onStateUpdate: { box.state = $0 } + ) + .frame(width: 420, height: 300)) + let window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 420, height: 300), styleMask: [.titled], + backing: .buffered, defer: false) + window.isReleasedWhenClosed = false + window.contentView = host + window.orderFrontRegardless() + defer { window.close() } + // Poll rather than wait a fixed time: CI machines are slower. + func wait(until done: () -> Bool) { + for _ in 0..<50 where !done() { RunLoop.main.run(until: Date().addingTimeInterval(0.1)) } + } + wait { window.attachedSheet != nil } + guard let sheet = window.attachedSheet else { + Issue.record("no sheet for $open = true") + return + } + // Escape, which the sheet's close button answers to. + sheet.makeKey() + for type in [NSEvent.EventType.keyDown, .keyUp] { + let escape = NSEvent.keyEvent( + with: type, location: .zero, modifierFlags: [], timestamp: 0, + windowNumber: sheet.windowNumber, context: nil, characters: "\u{1b}", + charactersIgnoringModifiers: "\u{1b}", isARepeat: false, keyCode: 53)! + sheet.sendEvent(escape) + } + wait { window.attachedSheet == nil && box.state["$open"] == false } + #expect(window.attachedSheet == nil) + #expect(box.state["$open"] == false) + } + + /// The editable table's keys with a keyboard: Down and Up move between + /// rows, and Enter keeps the edit and moves down. + @Test func editableTableKeys() { + let input = """ + root = Card([t]) + t = EditableTable("roster", [{type: "text", key: "name", header: "Name"}], [{id: "1", values: ["Alex"]}, {id: "2", values: ["Jamie"]}, {id: "3", values: ["Sam"]}]) + """ + let host = NSHostingView( + rootView: OpenUIRenderer(response: input, library: OpenUIChatLibrary.library) + .frame(width: 420, height: 300)) + let window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 420, height: 300), styleMask: [.titled], + backing: .buffered, defer: false) + window.isReleasedWhenClosed = false + window.contentView = host + window.makeKeyAndOrderFront(nil) + defer { window.close() } + func settle() { RunLoop.main.run(until: Date().addingTimeInterval(0.2)) } + func fields(_ view: NSView) -> [NSTextField] { + let own = (view as? NSTextField).map { [$0] } ?? [] + return own + view.subviews.flatMap(fields) + } + func key(_ code: UInt16, _ characters: String) { + for type in [NSEvent.EventType.keyDown, .keyUp] { + window.sendEvent( + NSEvent.keyEvent( + with: type, location: .zero, modifierFlags: [], timestamp: 0, + windowNumber: window.windowNumber, context: nil, characters: characters, + charactersIgnoringModifiers: characters, isARepeat: false, keyCode: code)!) + } + settle() + } + /// The text of the field being edited. + func focused() -> String? { (window.firstResponder as? NSText)?.string } + /// Waits for focus to land on the field showing `text` (CI is slower). + func focuses(_ text: String) -> Bool { + for _ in 0..<30 where focused() != text { settle() } + return focused() == text + } + settle() + let cells = fields(host).filter(\.isEditable).sorted { + $0.convert(.zero, to: nil).y > $1.convert(.zero, to: nil).y + } + guard cells.count == 3 else { + Issue.record("expected 3 cells, found \(cells.count)") + return + } + window.makeFirstResponder(cells[0]) + settle() + #expect(focuses("Alex")) + key(125, "\u{F701}") // Down + #expect(focuses("Jamie")) + key(126, "\u{F700}") // Up + #expect(focuses("Alex")) + (window.firstResponder as? NSTextView)?.insertText( + "x", replacementRange: NSRange(location: 4, length: 0)) + key(36, "\r") // Enter + #expect(focuses("Jamie")) + #expect(cells[0].stringValue == "Alexx") + key(125, "\u{F701}") // Down + key(125, "\u{F701}") // Down at the last row stays there + #expect(focuses("Sam")) + #expect(cells[0].stringValue == "Alexx") + } + + /// The bug vishxrad hit in the Angular port: an input recreated on every + /// update loses focus mid-typing. The AppKit text field behind the SwiftUI + /// input must be the same object across streamed updates and typing. + @Test func keepsInputViewsAcrossStreamingAndTyping() { + final class Model: ObservableObject { + @Published var response = """ + $name = "" + root = Card([form, note]) + form = Form("f", btns, [field]) + btns = Buttons([Button("Save")]) + field = FormControl("Name", Input("name", "Your name", "text", null, $name)) + """ + var state = OpenUIObject() + } + struct Harness: View { + @ObservedObject var model: Model + var body: some View { + OpenUIRenderer( + response: model.response, isStreaming: true, library: OpenUIChatLibrary.library, + onStateUpdate: { model.state = $0 } + ) + .frame(width: 420) + } + } + func textFields(_ view: NSView) -> [NSTextField] { + let own = (view as? NSTextField).map { [$0] } ?? [] + return own + view.subviews.flatMap(textFields) + } + + let model = Model() + let host = NSHostingView(rootView: Harness(model: model)) + let window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 420, height: 400), styleMask: [.borderless], + backing: .buffered, defer: false) + window.contentView = host + RunLoop.main.run(until: Date().addingTimeInterval(0.3)) + let field = textFields(host).first { $0.isEditable } + #expect(field != nil) + + // A later statement streams in after the field. + model.response += "\nnote = TextContent(\"Saved drafts appear here.\")" + RunLoop.main.run(until: Date().addingTimeInterval(0.3)) + #expect(textFields(host).first { $0.isEditable } === field) + + // Typing writes $name, which re-evaluates the tree. + field?.stringValue = "Ada" + field?.sendAction(field?.action, to: field?.target) + NotificationCenter.default.post(name: NSControl.textDidChangeNotification, object: field) + RunLoop.main.run(until: Date().addingTimeInterval(0.3)) + #expect(model.state["$name"] == "Ada") + #expect(textFields(host).first { $0.isEditable } === field) + } + + /// A finished response inside a stack (a restored chat message) must render + /// even though its text never changes after the view appears. + @Test func rendersAFinishedResponseNestedInAStack() { + let input = Self.examples[1].input + let host = NSHostingView( + rootView: VStack(alignment: .leading) { + Text("Earlier message") + OpenUIRenderer(response: input, library: OpenUIChatLibrary.library) + } + .frame(width: 420)) + let window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 420, height: 600), styleMask: [.borderless], + backing: .buffered, defer: false) + window.contentView = host + RunLoop.main.run(until: Date().addingTimeInterval(0.3)) + #expect(host.fittingSize.height > 400, "rendered \(host.fittingSize)") + } + + /// The response shows on the very first layout, before any onChange runs. + /// A renderer in a lazy stack is rebuilt each time it scrolls back into + /// view; if it came back empty and then grew, the stack would keep + /// re-placing rows. + @Test func rendersOnTheFirstFrame() { + let host = NSHostingView( + rootView: OpenUIRenderer( + response: Self.examples[1].input, library: OpenUIChatLibrary.library + ) + .frame(width: 420)) + #expect(host.fittingSize.height > 400, "first frame was \(host.fittingSize)") + } + + @Test func passesParseResultsAndErrorsToTheHost() { + final class Received { + var parseResults = 0 + var errors: [[OpenUIError]] = [] + } + let received = Received() + let host = NSHostingView( + rootView: OpenUIRenderer( + response: "root = Card([Mystery(), TextContent(\"ok\")])", + library: OpenUIChatLibrary.library, + onParseResult: { _ in received.parseResults += 1 }, + onError: { received.errors.append($0) })) + let window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 420, height: 300), styleMask: [.borderless], + backing: .buffered, defer: false) + window.contentView = host + RunLoop.main.run(until: Date().addingTimeInterval(0.3)) + #expect(received.parseResults == 1) + #expect(received.errors.map { $0.map(\.code) } == [["unknown-component"]]) + } + } +#endif + +/// Libraries are typically global constants; this must compile under Swift 6. +let globalTestLibrary = SwiftUILibrary( + components: [ + SwiftUIComponent(ComponentSchema("Box", description: "", props: [Prop("text", .string)])) { + Text($0.text("text")) + } + ], root: "Box") + +@Suite struct GlobalLibraryTests { + @Test func globalLibraryIsUsable() { + #expect(globalTestLibrary.paramMap["Box"]?.map(\.name) == ["text"]) + } +} + +@Suite struct CitationTests { + @Test func splitsLikeRemarkCitations() { + #expect( + CitedMarkdown.segments("Big [1][2] news [3]. End") + == [.text("Big "), .citation([1, 2]), .text("news "), .citation([3]), .text(". End")]) + #expect(CitedMarkdown.segments("No citations") == [.text("No citations")]) + #expect(CitedMarkdown.segments("[1] [2]") == [.citation([1, 2])]) + } +} + +@MainActor +@Suite struct MarkdownTests { + @Test func parsesMathAndTables() { + let blocks = MarkdownBlocks.parse( + "Intro\n$$\nA = P(1 + r)^n\n$$\n| a | b |\n|---|:-:|\n| 1 | 2 |\nAfter $$x$$") + #expect( + blocks == [ + .paragraph("Intro"), .math("A = P(1 + r)^n"), + .table(header: ["a", "b"], rows: [["1", "2"]]), + .paragraph("After $$x$$"), + ]) + #expect(MarkdownBlocks.parse("$$E = mc^2$$") == [.math("E = mc^2")]) + // Without a separator row it's just text. + #expect(MarkdownBlocks.parse("| not | a table |") == [.paragraph("| not | a table |")]) + } + + @Test func onlyDoubleTildesStrikeThrough() { + let lone = InlineMarkdown.attributed("about ~$5K and ~4 days") + #expect(String(lone.characters) == "about ~$5K and ~4 days") + let double = InlineMarkdown.attributed("~~gone~~") + #expect(String(double.characters) == "gone") + } + + @Test func scatterDomainPadsLikeReactUI() { + #expect(ScatterChartView.domain([150, 190]) == 146...194) + #expect(ScatterChartView.domain([1, 5]) == 0.6...5.4) + #expect(ScatterChartView.domain([]) == 0...100) + } +} + +/// Mirrors react-ui's paletteUtils tests. +@Suite struct ChartPaletteTests { + /// react-ui's `resolvePalette`: the chart's own palette, then the default + /// one, then the built-in ramp; an empty palette counts as none. + @Test func chartsUseTheirOwnPaletteFirst() { + var theme = OpenUITheme() + #expect(theme.chartRamp(theme.barChartPalette) == nil) + theme.chartPalette = [.red, .blue] + #expect(theme.chartRamp(theme.barChartPalette) == [.red, .blue]) + theme.barChartPalette = [.green] + #expect(theme.chartRamp(theme.barChartPalette) == [.green]) + #expect(theme.chartRamp(theme.lineChartPalette) == [.red, .blue]) + theme.barChartPalette = [] + #expect(theme.chartRamp(theme.barChartPalette) == [.red, .blue]) + } + + @Test func statusVariantsUseTheThemeColors() { + var theme = OpenUITheme() + theme.info = .cyan + theme.success = .mint + theme.alert = .yellow + theme.danger = .pink + let variants: [String?] = ["info", "success", "warning", "error", "danger", "neutral", nil] + let colors: [Color] = [.cyan, .mint, .yellow, .pink, .pink, .secondary, .secondary] + #expect(variants.map(theme.status) == colors) + } + + let ramp = ChartPalette.ocean + + @Test func picksFromTheMiddleOutwards() { + #expect(ChartPalette.colors(1) == [ramp[5]]) + #expect(ChartPalette.colors(2) == [ramp[4], ramp[6]]) + #expect(ChartPalette.colors(3) == [ramp[4], ramp[5], ramp[6]]) + #expect(ChartPalette.colors(5) == Array(ramp[3...7])) + } + + @Test func usesTheThemePaletteAndFallsBackWhenEmpty() { + let pastel: [Color] = [.pink, .mint, .yellow] + #expect(ChartPalette.colors(3, pastel) == pastel) + #expect(ChartPalette.colors(2, [.pink, .mint]) == [.pink, .mint]) + #expect(ChartPalette.colors(1, []) == [ramp[5]]) + } + + @Test func neverIndexesOutOfBounds() { + for size in 1...12 { + let palette = (0.. [String: SyntaxHighlighter.Token] { + var kinds: [String: SyntaxHighlighter.Token] = [:] + for (text, token) in SyntaxHighlighter.tokenize(code, language: language) + where !text.trimmingCharacters(in: .whitespaces).isEmpty { + kinds[text] = token + } + return kinds + } + + @Test func keepsTheSourceIntact() { + for (code, language) in [ + ("const total = items.map(x => x * 2) // twice\n", "ts"), + ("def f(x):\n \"\"\"doc\"\"\"\n return x # done", "python"), + ("{\"a\": [1, true, null]}", "json"), ("Hi", "html"), + ("SELECT name FROM users WHERE id = 3", "sql"), ("let s = \"unfinished", "swift"), + ] { + let joined = SyntaxHighlighter.tokenize(code, language: language).map(\.0).joined() + #expect(joined == code) + } + } + + @Test func classifiesTokensLikePrism() { + let ts = kinds("import { x } from \"y\"\nconst total = sum(3.5) // note", "ts") + #expect(ts["import"] == .control) + #expect(ts["const"] == .keyword) + #expect(ts["\"y\""] == .string) + #expect(ts["sum"] == .function) + #expect(ts["3.5"] == .number) + #expect(ts["// note"] == .comment) + #expect(ts["total"] == .plain) + + let python = kinds("def greet(name):\n return None # nothing", "python") + #expect(python["def"] == .keyword) + #expect(python["greet"] == .function) + #expect(python["return"] == .control) + #expect(python["None"] == .constant) + #expect(python["# nothing"] == .comment) + + let json = kinds("{\"name\": \"Ada\", \"age\": 36, \"ok\": true}", "json") + #expect(json["\"name\""] == .property) + #expect(json["\"Ada\""] == .string) + #expect(json["36"] == .number) + #expect(json["true"] == .constant) + + let html = kinds("Hi", "html") + #expect(html[" bounds.width * bounds.height * 0.9) + } + + @Test func usesReactUITemplates() { + // Five or more, wide: 2fr 1fr 1fr with the first image down both rows. + let wide = GalleryMosaic.frames(count: 5, in: CGRect(x: 0, y: 0, width: 708, height: 376)) + #expect(wide[0] == CGRect(x: 0, y: 0, width: 346, height: 376)) + #expect(wide[1] == CGRect(x: 354, y: 0, width: 173, height: 184)) + #expect(wide[4] == CGRect(x: 535, y: 192, width: 173, height: 184)) + // Narrow: two on top, three below. + let narrow = GalleryMosaic.frames(count: 5, in: CGRect(x: 0, y: 0, width: 368, height: 288)) + #expect(narrow[0].width == narrow[1].width) + #expect(narrow[2].minY == narrow[3].minY && narrow[3].minY == narrow[4].minY) + #expect(GalleryMosaic.height(width: 1200, count: 5) == GalleryMosaic.maxHeight) + } +} + +/// Chart interactions against react-ui: its number formats (fixtures from its +/// formatters), legend folding and toggling, and where pie slices, stacked +/// bar segments and tooltips land. Main actor, because the legend's helpers +/// are members of a view. +@MainActor +@Suite struct ChartInteractionTests { + private struct FormatCase: Decodable { + let value: Double + let tick: String + let tooltip: String + } + + @Test func formatsNumbersLikeReactUI() throws { + let url = Bundle.module.url( + forResource: "chart-formats", withExtension: "json", subdirectory: "Fixtures")! + let cases = try JSONDecoder().decode([FormatCase].self, from: Data(contentsOf: url)) + #expect(cases.count > 20) + for item in cases { + #expect(ChartFormat.tick(item.value) == item.tick, "tick \(item.value)") + #expect( + ChartFormat.tooltip(item.value, locale: Locale(identifier: "en_US")) == item.tooltip, + "tooltip \(item.value)") + } + } + + @Test func keepsOneSeriesVisible() { + let keys = ["a", "b", "c"] + var hidden = ChartLegend.toggling("a", in: [], keys: keys) + hidden = ChartLegend.toggling("b", in: hidden, keys: keys) + #expect(hidden == ["a", "b"]) + #expect(ChartLegend.toggling("c", in: hidden, keys: keys) == ["a", "b"]) + #expect(ChartLegend.toggling("a", in: hidden, keys: keys) == ["b"]) + // Keys from older data don't count against the current series. + #expect(ChartLegend.toggling("a", in: ["gone"], keys: ["a", "b"]) == ["gone", "a"]) + } + + @Test func foldsTheLegendLikeUseDefaultLegend() { + // Each key's width already includes a gap; keys after the first add another. + #expect(ChartLegend.fittingCount([50, 50, 50], available: 174, button: 60) == 3) + #expect(ChartLegend.fittingCount([50, 50, 50], available: 173, button: 60) == 2) + #expect(ChartLegend.fittingCount([50, 50, 50, 50], available: 200, button: 40) == 2) + #expect(ChartLegend.fittingCount([300, 50], available: 200, button: 40) == 1) + #expect(ChartLegend.fittingCount([50, 50], available: 0, button: 40) == 2) + } + + @Test func sortsSlicesLargestFirst() { + let slices = [ + Slice(id: 0, label: "a", value: 1), Slice(id: 1, label: "b", value: 5), + Slice(id: 2, label: "c", value: 5), Slice(id: 3, label: "d", value: 3), + ] + let sorted = sortedSlices(slices, nil) + #expect(sorted.map(\.slice.label) == ["b", "c", "d", "a"]) + #expect(sorted.map(\.color) == ChartPalette.colors(4)) + } + + @Test func findsPieSlicesClockwiseFromTwelve() { + let size = CGSize(width: 200, height: 200) + let values: [Double] = [1, 1, 2] + func slice(_ x: CGFloat, _ y: CGFloat, inner: CGFloat = 0, semi: Bool = false) -> Int? { + PieGeometry.slice( + at: CGPoint(x: x, y: y), in: size, values: values, innerRatio: inner, semicircle: semi) + } + #expect(slice(150, 60) == 0) // upper right + #expect(slice(150, 140) == 1) // lower right + #expect(slice(50, 100) == 2) // left + #expect(slice(100, 100, inner: 0.6) == nil) // the donut's hole + #expect(slice(5, 5) == nil) // outside the circle + // A semicircle is turned a quarter to the left: it starts at 9 o'clock. + #expect(slice(40, 90, semi: true) == 0) + #expect(slice(160, 90, semi: true) == 2) + #expect(slice(100, 160, semi: true) == nil) // the empty lower half + } + + @Test func splitsTheStackedBarByShare() { + let widths = SegmentRow.widths(204, [0.5, 0.25, 0.25]) + #expect(widths == [100, 50, 50]) + #expect(SegmentRow.index(at: 10, width: 204, shares: [0.5, 0.25, 0.25]) == 0) + #expect(SegmentRow.index(at: 120, width: 204, shares: [0.5, 0.25, 0.25]) == 1) + #expect(SegmentRow.index(at: 200, width: 204, shares: [0.5, 0.25, 0.25]) == 2) + } + + @Test func placesTooltipsLikeFloatingUI() { + let bounds = CGRect(x: 0, y: 0, width: 400, height: 200) + let size = CGSize(width: 150, height: 80) + // Right of the pointer, flipped left near the trailing edge, kept inside. + #expect( + TooltipPlacement.origin(for: size, anchor: CGPoint(x: 50, y: 30), in: bounds) + == CGPoint(x: 70, y: 30)) + #expect( + TooltipPlacement.origin(for: size, anchor: CGPoint(x: 300, y: 30), in: bounds) + == CGPoint(x: 130, y: 30)) + #expect( + TooltipPlacement.origin(for: size, anchor: CGPoint(x: 300, y: 190), in: bounds) + == CGPoint(x: 130, y: 120)) + #expect( + TooltipPlacement.origin( + for: size, anchor: CGPoint(x: 10, y: 10), placement: .above, in: bounds) + == CGPoint(x: 8, y: -90)) + } +} + +#if canImport(AppKit) + /// Wrapping rows (tags, chips, legends) never let a child spill past the + /// container: one wider than a row gets the row's width. + @MainActor + @Suite struct FlowLayoutTests { + @Test func capsWideChildrenAtTheRowWidth() { + final class Box { var width: CGFloat = 0 } + let box = Box() + let host = NSHostingView( + rootView: FlowLayout { + Text("Short") + Text(String(repeating: "wide ", count: 60)) + .lineLimit(1) + .onGeometryChange(for: CGFloat.self) { + $0.size.width + } action: { + box.width = $0 + } + } + .frame(width: 200)) + let window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 200, height: 200), styleMask: [.borderless], + backing: .buffered, defer: false) + window.contentView = host + RunLoop.main.run(until: Date().addingTimeInterval(0.2)) + #expect(box.width > 150) + #expect(box.width <= 200) + } + } +#endif + +/// The flex layout behind openuiLibrary's Stack and Card, against what CSS +/// flexbox does with react-ui's styles. +@Suite struct FlexLayoutTests { + typealias Item = FlexLayout.Item + + @Test func growingItemsShareTheFreeSpace() { + // Two cards (flex: 1, basis 0) and a 100pt button in 400pt with 12pt gaps. + let items = [ + Item(basis: 0, flexible: true), Item(basis: 100, flexible: false), + Item(basis: 0, flexible: true), + ] + let (widths, offsets) = FlexLayout.distribute(items, width: 400, gap: 12, justify: nil) + #expect(widths == [138, 100, 138]) + #expect(offsets == [0, 150, 262]) + } + + @Test func justifySpreadsSpaceWhenNothingGrows() { + let items = [Item(basis: 100, flexible: false), Item(basis: 100, flexible: false)] + func offsets(_ justify: String?) -> [CGFloat] { + FlexLayout.distribute(items, width: 300, gap: 0, justify: justify).offsets + } + #expect(offsets(nil) == [0, 100]) + #expect(offsets("center") == [50, 150]) + #expect(offsets("end") == [100, 200]) + #expect(offsets("between") == [0, 200]) + #expect(offsets("around") == [25, 175]) + let third: CGFloat = 100 / 3 + #expect(offsets("evenly") == [third, 100 + 2 * third]) + } + + @Test func fixedItemsShrinkInProportion() { + let items = [Item(basis: 300, flexible: false), Item(basis: 100, flexible: false)] + #expect(FlexLayout.distribute(items, width: 200, gap: 0, justify: nil).widths == [150, 50]) + } + + @Test func wrapsByBasisButGrowingItemsNeverWrap() { + let fixed = Array(repeating: Item(basis: 100, flexible: false), count: 5) + #expect(FlexLayout.lines(fixed, width: 330, gap: 10, wrap: true) == [0..<3, 3..<5]) + #expect(FlexLayout.lines(fixed, width: 330, gap: 10, wrap: false) == [0..<5]) + let cards = Array(repeating: Item(basis: 0, flexible: true), count: 5) + #expect(FlexLayout.lines(cards, width: 100, gap: 10, wrap: true) == [0..<5]) + } + + @Test func gapsFollowReactUISpacing() { + let names: [String?] = ["none", "xs", "s", nil, "m", "l", "xl", "2xl"] + let gaps: [CGFloat] = [0, 6, 8, 12, 12, 18, 24, 36] + #expect(names.map(FlexLayout.gap) == gaps) + } + +} + +/// The editable table's edit bookkeeping. +@Suite struct TableEditsTests { + @Test func onlyChangedTextIsAnEdit() { + var edits = TableEdits() + // A focused field writes its text back unchanged: not an edit. + edits.set("Alex", row: "1", column: 0, original: "Alex") + #expect(edits.count == 0) + edits.set("Alexx", row: "1", column: 0, original: "Alex") + edits.set("Ops", row: "2", column: 1, original: "Eng") + #expect(edits.count == 2) + #expect(edits["1", 0] == "Alexx") + // Typing the original back undoes the edit. + edits.set("Alex", row: "1", column: 0, original: "Alex") + #expect(edits.count == 1) + #expect(edits["1", 0] == nil) + } +} + +/// Card grids keep react-ui's breakpoints but never squeeze a card below its +/// react-ui minimum width. +@Suite struct CardGridColumnsTests { + func columns(_ width: CGFloat, responsive: Bool, min: CGFloat = 0) -> Int { + CardGridLayout.columns( + width: width, maxPerRow: 3, responsive: responsive, spacing: 12, minCellWidth: min) + } + + @Test func followsReactUIBreakpoints() { + #expect(columns(400, responsive: true) == 1) + #expect(columns(700, responsive: true) == 2) + #expect(columns(900, responsive: true) == 3) + #expect(columns(400, responsive: false) == 3) + } + + /// A context card (196pt minimum) in a non-responsive block on a phone: one + /// per row instead of three a word wide. + @Test func dropsColumnsBelowTheCardsMinimum() { + #expect(columns(350, responsive: false, min: 196) == 1) + #expect(columns(404, responsive: false, min: 196) == 2) + #expect(columns(612, responsive: false, min: 196) == 3) + #expect(columns(900, responsive: true, min: 196) == 3) + #expect(columns(10, responsive: false, min: 196) == 1) + } +} + +/// react-ui's condensed x-axis labels: crowded categories label every n-th +/// one, each truncated to the room it then has. +@Suite struct CategoryLabelsTests { + @Test func labelsEveryCategoryWhenTheyFit() { + let layout = CategoryLabels.layout(widest: 30, slot: 40, count: 6) + #expect(layout.interval == 1) + #expect(layout.width == 32) + } + + /// Eight cities on a phone: every second one, with room for "Mumbai". + @Test func thinsCrowdedLabels() { + let layout = CategoryLabels.layout(widest: 70, slot: 40, count: 8) + #expect(layout.interval == 2) + #expect(layout.width == 72) + // react-ui's rule alone when the labels then fit: 48pt needed in 10pt slots. + #expect(CategoryLabels.layout(widest: 40, slot: 10, count: 20).interval == 5) + } + + /// Past react-ui's rule: thinning further beats cutting every label off, + /// down to three labels. + @Test func thinsRatherThanTruncates() { + // Seven "Month n" labels, 48pt wide, in 53pt slots: every second one. + let months = CategoryLabels.layout(widest: 48, slot: 53, count: 7) + #expect(months.interval == 2) + #expect(months.width >= 48) + // Labels too long for any thinning keep at least three. + #expect(CategoryLabels.layout(widest: 500, slot: 40, count: 9).interval == 3) + } + + @Test func neverSkipsPastTheLastCategory() { + #expect(CategoryLabels.layout(widest: 100, slot: 2, count: 3).interval == 3) + #expect(CategoryLabels.layout(widest: 100, slot: 0, count: 3).interval == 1) + } + + /// Labels center on their category and stay inside the plot, as react-ui's + /// CondensedXAxis keeps them. + @Test func keepsLabelsInsideThePlot() { + // In the middle: centered, truncated to its box when it's too long. + #expect(CategoryLabels.place(text: 30, center: 100, width: 72, plot: 300) == (30, 0)) + #expect(CategoryLabels.place(text: 100, center: 150, width: 72, plot: 300) == (72, 0)) + // The first label's box is cut at the start, and the label moves in. + #expect(CategoryLabels.place(text: 60, center: 20, width: 72, plot: 300) == (56, 8)) + // The last one is cut at the end and moves back. + #expect(CategoryLabels.place(text: 50, center: 290, width: 72, plot: 300) == (46, -13)) + } +}