Skip to main content

everruns_a2ui/
lib.rs

1//! A2UI component catalog and prompt generator for Everruns.
2//!
3//! Produces system prompts that instruct LLMs to emit A2UI JSON component trees.
4//! The JSON is transported in ```a2ui fenced code blocks and rendered by the UI
5//! using native shadcn/ui primitives.
6//!
7//! This crate is part of the [Everruns](https://everruns.com) ecosystem.
8//!
9//! # Example
10//!
11//! ```
12//! use everruns_a2ui::{PromptOptions, default_catalog, generate_prompt};
13//!
14//! let prompt = generate_prompt(default_catalog(), &PromptOptions::default());
15//! assert!(prompt.contains("```a2ui"));
16//! ```
17//!
18//! Ref: specs/a2ui.md
19//! Ref: <https://github.com/google/a2ui>
20
21mod catalog;
22mod components;
23mod prompt;
24
25pub use catalog::{Catalog, ComponentCategory};
26pub use components::{ComponentDef, PropDef};
27pub use prompt::{PromptOptions, generate_prompt};
28
29use std::sync::LazyLock;
30
31/// The default A2UI catalog.
32pub fn default_catalog() -> &'static Catalog {
33    &DEFAULT_CATALOG
34}
35
36static DEFAULT_CATALOG: LazyLock<Catalog> = LazyLock::new(|| Catalog {
37    root_hint: "Card",
38    components: components::all_components(),
39    categories: catalog::default_categories(),
40});
41
42/// Generate the default A2UI system prompt with standard options.
43pub fn default_prompt() -> &'static str {
44    &DEFAULT_PROMPT
45}
46
47static DEFAULT_PROMPT: LazyLock<String> =
48    LazyLock::new(|| generate_prompt(default_catalog(), &PromptOptions::default()));
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53
54    #[test]
55    fn default_catalog_has_components() {
56        let cat = default_catalog();
57        assert!(!cat.components.is_empty());
58    }
59
60    #[test]
61    fn default_catalog_contains_core_components() {
62        let cat = default_catalog();
63        let names: Vec<&str> = cat.components.iter().map(|c| c.name).collect();
64        for expected in [
65            "Card",
66            "Stack",
67            "Heading",
68            "Text",
69            "Button",
70            "ButtonGroup",
71            "List",
72            "ListItem",
73            "Table",
74            "Form",
75            "TextField",
76            "Select",
77            "Checkbox",
78            "Callout",
79            "Badge",
80        ] {
81            assert!(names.contains(&expected), "missing component: {expected}");
82        }
83    }
84
85    #[test]
86    fn default_prompt_is_substantial() {
87        let prompt = default_prompt();
88        assert!(
89            prompt.len() > 800,
90            "prompt too short: {} chars",
91            prompt.len()
92        );
93    }
94
95    #[test]
96    fn default_prompt_mentions_a2ui_fence() {
97        assert!(default_prompt().contains("```a2ui"));
98    }
99
100    #[test]
101    fn default_prompt_mentions_json_shape() {
102        let prompt = default_prompt();
103        assert!(prompt.contains("\"type\""));
104        assert!(prompt.contains("\"props\""));
105        assert!(prompt.contains("\"children\""));
106    }
107
108    #[test]
109    fn default_prompt_lists_every_component() {
110        let cat = default_catalog();
111        let prompt = default_prompt();
112        for comp in &cat.components {
113            assert!(
114                prompt.contains(comp.name),
115                "prompt missing component {}",
116                comp.name
117            );
118        }
119    }
120
121    #[test]
122    fn default_prompt_includes_action_types() {
123        let prompt = default_prompt();
124        assert!(prompt.contains("\"message\""));
125        assert!(prompt.contains("\"open_url\""));
126    }
127
128    #[test]
129    fn categories_cover_all_components() {
130        let cat = default_catalog();
131        let grouped: std::collections::HashSet<&str> = cat
132            .categories
133            .iter()
134            .flat_map(|g| g.components.iter().copied())
135            .collect();
136        for comp in &cat.components {
137            assert!(
138                grouped.contains(comp.name),
139                "component '{}' not in any category",
140                comp.name
141            );
142        }
143    }
144}