Vectrum
Sign In
57 lines · 4.5 KB · Rust
//! OpenUI Lang, the small language agents write ` ```openui ` blocks in:
//! its parser and evaluator, kept free of GPUI so they can be tested on
//! their own. `ui::inline` renders what they produce.

pub mod eval;
pub mod lang;

/// What agents are told about rich blocks, when the setting is on: in the
/// system prompt for Claude and Pi, and with jcode's first message.
pub const INSTRUCTIONS: &str = r##"## Inline UI in Vectrum Code

Vectrum Code shows your Markdown answers and renders some fenced code blocks live. Plain Markdown comes first; use these only when a picture, table or control says it better than prose: architecture and flows, comparisons, results data, choices for the user to make.

- ```mermaid: a diagram (flowchart, sequenceDiagram, classDiagram, stateDiagram-v2, erDiagram, gantt, pie, mindmap).
- ```svg: a small self-contained SVG image, with width and height set.
- ```html: a self-contained page with inline CSS and JS. It shows as code; the user can open it with Preview.
- ```openui: native UI written in OpenUI Lang, below.

OpenUI Lang: one statement per line, `name = Expression`, `root = ...` first; forward references are fine. Arguments are positional, and optional ones can be left off the end. Values: strings, numbers, true/false/null, [arrays], {key: value} objects, references to other statements, `a.b` (on an array it takes `b` from every element), `cond ? a : b`, arithmetic, comparisons, && || !. State: declare `$name = default` and read `$name`; an input given `$name` as its first argument edits it, and everything reading it updates.
Built-ins: @Count @Sum @Avg @Min @Max @First @Last @Round(n, decimals) @Abs @Floor @Ceil @Filter(array, field, op, value) @Sort(array, field, "asc"|"desc") @Each(array, "item", template).
Actions, a Button's second argument: Action([steps]) with @Set($x, value), @Reset($x), @ToAssistant("message"), @OpenUrl("https://..."); or "submit:formName" to send a form's values to you, or "action:name".
Components:
- Layout: Stack(children, direction "column"|"row", gap "s"|"m"|"l", align, justify, wrap), Card(children, variant "raised"|"well"|"plain"), CardHeader(title, subtitle), Tabs([TabItem(value, label, content)]), Separator().
- Content: TextContent(text, size "small"|"default"|"large"|"large-heavy"), Markdown(text), Callout(variant "info"|"success"|"warning"|"error", title, description), Tag(text, variant), Stat(label, value, delta), Progress(value 0..1 or 0..100, label), Image(url, alt), CodeBlock(language, code).
- Data: Table([Col(label, values, type "text"|"number"|"tag")]), BarChart(labels, [Series(name, values)], "grouped"|"stacked", xLabel, yLabel), LineChart and AreaChart(labels, [Series(name, values)], xLabel, yLabel), PieChart(labels, values).
- Input: Form(name, [fields], Buttons([...])), FormControl(label, input, hint), Input(name, placeholder, type "text"|"email"|"number"|"password", rules ["required", "email"]), TextArea(name, placeholder, rows), Select(name, options, placeholder), Checkbox(name, label), Switch(name, label), Slider(name, min, max, step), Buttons([buttons], direction), Button(label, action, variant "primary"|"secondary"|"danger"|"ghost").
Query, Mutation and @Run aren't available. When the user acts on your UI you get a message starting with [UI], such as `[UI] Submitted "deploy": {"env":"staging"}`. Answer it in a new message; earlier blocks keep their state.

Examples:
```openui
root = Card([header, form])
header = CardHeader("Deploy", "Pick a target")
form = Form("deploy", [FormControl("Environment", Select("env", ["staging", "production"]))], Buttons([Button("Deploy", "submit:deploy", "primary")]))
```
```openui
$count = 0
root = Stack([TextContent("Count: " + $count, "large"), Buttons([Button("-", Action([@Set($count, $count - 1)])), Button("+", Action([@Set($count, $count + 1)]), "primary")], "row")])
```"##;

#[cfg(test)]
mod tests {
    use super::{INSTRUCTIONS, eval, lang};

    /// The examples agents are shown must parse and render.
    #[::core::prelude::v1::test]
    fn instruction_examples_parse() {
        let blocks: Vec<&str> = INSTRUCTIONS.split("```openui\n").skip(1).map(|block| block.split("```").next().unwrap_or_default()).collect();
        assert_eq!(blocks.len(), 2);
        for block in blocks {
            let program = lang::parse(block);
            assert!(program.errors.is_empty(), "{block}: {:?}", program.errors);
            assert!(program.undefined().is_empty(), "{block}");
            assert!(!eval::evaluate(&program, &eval::State::new()).is_pending());
        }
        assert!(INSTRUCTIONS.lines().count() < 60);
    }
}