UiRenderer

Renders a window from a JSON spec, for example one an assistant produced: a tree of library components with state, bindings, conditions and actions. Nothing has to exist in your code beforehand, and only allowlisted components and props can be used.

import { UiRenderer } from 'jarvis-react-ui/generative';

Basic

A spec has an id, initial state and a root node. Each node names a component (type) and sets its props; { "$bind": "volume" } binds a control to a state value in both directions, and visibleIf shows a node only while a condition holds. Text may contain {{key}} templates.

import { UiRenderer } from 'jarvis-react-ui/generative';
import type { UiWindowSpec } from 'jarvis-react-ui/generative';

// The kind of JSON an assistant returns when asked for "a volume control".
const SPEC: UiWindowSpec = {
    id: 'volume',
    state: { volume: 40, muted: false },
    root: {
        type: 'Stack',
        props: { gap: 'md' },
        children: [
            {
                type: 'Slider',
                props: {
                    label: 'Volume',
                    value: { $bind: 'volume' },
                    showValue: true,
                    formatValue: '{value} %',
                },
            },
            { type: 'Switch', props: { label: 'Mute', checked: { $bind: 'muted' } } },
            {
                type: 'Pill',
                props: { variant: 'warn' },
                children: 'Muted',
                visibleIf: { state: 'muted' },
            },
            {
                type: 'Mono',
                children: 'Output at {{volume}} %',
                visibleIf: { state: 'muted', eq: false },
            },
        ],
    },
};

export default function Basic() {
    return <UiRenderer spec={SPEC} />;
}

The spec

{
    "id": "volume",
    "title": "Volume",
    "size": { "w": 360, "h": 240 },
    "state": { "volume": 40, "muted": false },
    "root": {
        "type": "Stack",
        "children": [
            {
                "type": "Slider",
                "props": {
                    "label": "Volume",
                    "value": { "$bind": "volume" },
                    "formatValue": "{value} %"
                }
            },
            {
                "type": "Mono",
                "children": "Output at {{volume}} %",
                "visibleIf": { "state": "muted", "eq": false }
            },
            {
                "type": "Button",
                "children": "Apply",
                "on": { "onClick": { "emit": "apply", "payload": { "$state": "volume" } } }
            }
        ]
    }
}
Part Meaning
type A component from the allowlist (UI_REGISTRY); others render a placeholder.
props Allowed props of that component; others are ignored.
{ "$state": "k" } Reads a state value.
{ "$bind": "k" } Two-way binding on value / checked; the change handler is wired for you.
"…{{k}}…" Template in any text.
children Text, or an array of nodes for layout components.
on Actions per event prop: { "set": "k", "value"? }, { "toggle": "k" }, { "emit": "name", "payload"? }.
visibleIf { "state": "k" } (truthy), { "state": "k", "eq": v } or { "state": "k", "ne": v }.
id Makes a player addressable through call(...).

Some props are translated: text props (label, title, …) take text or a nested node, icon props take a name from UI_ICONS ("Play"), format functions take a template ("{value} %"), Table takes columns: [{ key, header, align? }] and rows objects, and Tabs items take text or a node as content.

Several windows

useUiWindows() manages generated windows for a WindowManager: open validates a spec and opens it as a floating window (an existing id is replaced), patchState changes a window's state without rebuilding it, call controls players, and onAction receives every emit. Try opening both windows, then the buttons.

import { useState } from 'react';
import { Button, Mono, Stack, WindowManager } from 'jarvis-react-ui';
import { useUiWindows } from 'jarvis-react-ui/generative';
import type { UiWindowSpec } from 'jarvis-react-ui/generative';

const VIDEO: UiWindowSpec = {
    id: 'video',
    title: 'Video',
    size: { w: 420, h: 320 },
    root: { type: 'YouTubePlayer', id: 'player', props: { videoId: 'aqz-KE-bpKQ', size: 'sm' } },
};

const VOLUME: UiWindowSpec = {
    id: 'volume',
    title: 'Volume',
    size: { w: 280, h: 160 },
    state: { volume: 40 },
    root: {
        type: 'Stack',
        children: [
            {
                type: 'Slider',
                props: { label: 'Volume', value: { $bind: 'volume' }, showValue: true },
            },
            {
                type: 'Button',
                children: 'Apply',
                on: { onClick: { emit: 'apply_volume', payload: { $state: 'volume' } } },
            },
        ],
    },
};

export default function Windows() {
    const [log, setLog] = useState('—');
    const ui = useUiWindows({
        onAction: (e) => setLog(`${e.windowId} → ${e.name}(${JSON.stringify(e.payload)})`),
    });

    return (
        <>
            <Stack direction="row" gap="sm" wrap className="absolute top-3 left-3 z-[1]">
                <Button size="sm" onClick={() => ui.open(VIDEO)}>
                    Open video
                </Button>
                <Button size="sm" onClick={() => ui.open(VOLUME)}>
                    Open volume
                </Button>
                <Button size="sm" onClick={() => ui.patchState('volume', { volume: 20 })}>
                    Set volume 20
                </Button>
                <Button size="sm" onClick={() => ui.call('video', 'player', 'seek', 90)}>
                    Jump to 1:30
                </Button>
                <Mono size="xs" secondary>
                    {log}
                </Mono>
            </Stack>
            <WindowManager
                windows={ui.windows}
                assignments={{}}
                onAssignmentsChange={() => undefined}
                onClose={ui.onClose}
            />
        </>
    );
}

Validation

validateUiSpec(spec) returns every problem with its JSON path, so you can send them back to the model and ask for a fixed spec. open refuses invalid specs; UiRenderer renders what it can and reports the rest through onError.

import { CodeBlock } from 'jarvis-react-ui';
import { validateUiSpec } from 'jarvis-react-ui/generative';

// A spec with mistakes, as a model might produce it.
const SPEC = {
    id: 'broken',
    state: { level: 3 },
    root: {
        type: 'Stack',
        children: [
            { type: 'ThreeOrb' },
            { type: 'Slider', props: { value: { $bind: 'volume' }, color: 'red' } },
            { type: 'Link', props: { href: 'javascript:alert(1)' }, children: 'Click' },
        ],
    },
};

export default function Validation() {
    const { errors } = validateUiSpec(SPEC);
    const text = errors.map((e) => `${e.path}: ${e.message}`).join('\n');
    return <CodeBlock code={text} language="text" title="validateUiSpec(spec).errors" />;
}

Safety

  • Only components and props in UI_REGISTRY are rendered. There is no way to set className, inline styles, raw HTML (CodeBlock.html is not allowed) or event handlers other than actions.
  • Links, media sources and web frames accept only http(s) URLs or relative paths; WebFrame's sandbox cannot be changed from a spec.
  • Text is always rendered as text. Specs are limited to 24 levels and 500 nodes.

Accessibility

The rendered components keep their own semantics and keyboard support. Ask the model for labels: label on form controls, aria-label where there is no visible text.

API

Prop Type Default Description
spec (required) UiWindowSpec The window spec to render; only its root and initial state are used here.
onAction ((event: UiActionEvent) => void) Called for every emit action.
onError ((errors: UiSpecError[]) => void) Called with the problems validateUiSpec finds; invalid parts render as placeholders.
onStateChange ((state: UiState) => void) Called with the full next state whenever a binding or a set / toggle action changes it.
state UiState Controlled state. Leave undefined to keep the state inside the renderer, starting from spec.state.

Also accepts inherited props from RefAttributes (1), Attributes (1).