1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4
5pub const MIME_TYPE_UI: &str = "application/vnd.adk.ui+json";
6pub const MIME_TYPE_UI_UPDATE: &str = "application/vnd.adk.ui.update+json";
7
8#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
9#[serde(tag = "type", rename_all = "snake_case")]
10pub enum Component {
11 Text(Text),
13 Button(Button),
14 Icon(Icon),
15 Image(Image),
16 Badge(Badge),
17
18 TextInput(TextInput),
20 NumberInput(NumberInput),
21 Select(Select),
22 MultiSelect(MultiSelect),
23 Switch(Switch),
24 DateInput(DateInput),
25 Slider(Slider),
26
27 Stack(Stack),
29 Grid(Grid),
30 Card(Card),
31 Container(Container),
32 Divider(Divider),
33 Tabs(Tabs),
34
35 Table(Table),
37 List(List),
38 KeyValue(KeyValue),
39 CodeBlock(CodeBlock),
40
41 Chart(Chart),
43
44 Alert(Alert),
46 Progress(Progress),
47 Toast(Toast),
48 Modal(Modal),
49 Spinner(Spinner),
50 Skeleton(Skeleton),
51
52 Textarea(Textarea),
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
59pub struct Text {
60 #[serde(skip_serializing_if = "Option::is_none")]
62 pub id: Option<String>,
63 pub content: String,
64 #[serde(default)]
65 pub variant: TextVariant,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
69#[serde(rename_all = "snake_case")]
70pub enum TextVariant {
71 H1,
72 H2,
73 H3,
74 H4,
75 #[default]
76 Body,
77 Caption,
78 Code,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
82pub struct Button {
83 #[serde(skip_serializing_if = "Option::is_none")]
85 pub id: Option<String>,
86 pub label: String,
87 pub action_id: String,
88 #[serde(default)]
89 pub variant: ButtonVariant,
90 #[serde(default)]
91 pub disabled: bool,
92 #[serde(skip_serializing_if = "Option::is_none")]
94 pub icon: Option<String>,
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
98#[serde(rename_all = "snake_case")]
99pub enum ButtonVariant {
100 #[default]
101 Primary,
102 Secondary,
103 Danger,
104 Ghost,
105 Outline,
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
109pub struct Icon {
110 #[serde(skip_serializing_if = "Option::is_none")]
112 pub id: Option<String>,
113 pub name: String, #[serde(default)]
115 pub size: u8,
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
119pub struct Image {
120 #[serde(skip_serializing_if = "Option::is_none")]
122 pub id: Option<String>,
123 pub src: String,
124 pub alt: Option<String>,
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
128pub struct Badge {
129 #[serde(skip_serializing_if = "Option::is_none")]
131 pub id: Option<String>,
132 pub label: String,
133 #[serde(default)]
134 pub variant: BadgeVariant,
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
138#[serde(rename_all = "snake_case")]
139pub enum BadgeVariant {
140 #[default]
141 Default,
142 Info,
143 Success,
144 Warning,
145 Error,
146 Secondary,
147 Outline,
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
153pub struct TextInput {
154 #[serde(skip_serializing_if = "Option::is_none")]
156 pub id: Option<String>,
157 pub name: String,
158 pub label: String,
159 #[serde(default = "default_input_type")]
161 pub input_type: String,
162 #[serde(skip_serializing_if = "Option::is_none")]
163 pub placeholder: Option<String>,
164 #[serde(default)]
165 pub required: bool,
166 #[serde(skip_serializing_if = "Option::is_none")]
167 pub default_value: Option<String>,
168 #[serde(skip_serializing_if = "Option::is_none")]
170 pub min_length: Option<usize>,
171 #[serde(skip_serializing_if = "Option::is_none")]
173 pub max_length: Option<usize>,
174 #[serde(skip_serializing_if = "Option::is_none")]
175 pub error: Option<String>,
176}
177
178fn default_input_type() -> String {
179 "text".to_string()
180}
181
182#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
183pub struct NumberInput {
184 #[serde(skip_serializing_if = "Option::is_none")]
186 pub id: Option<String>,
187 pub name: String,
188 pub label: String,
189 #[serde(skip_serializing_if = "Option::is_none")]
190 pub min: Option<f64>,
191 #[serde(skip_serializing_if = "Option::is_none")]
192 pub max: Option<f64>,
193 #[serde(skip_serializing_if = "Option::is_none")]
194 pub step: Option<f64>,
195 #[serde(default)]
196 pub required: bool,
197 #[serde(skip_serializing_if = "Option::is_none")]
199 pub default_value: Option<f64>,
200 #[serde(skip_serializing_if = "Option::is_none")]
201 pub error: Option<String>,
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
205pub struct Select {
206 #[serde(skip_serializing_if = "Option::is_none")]
208 pub id: Option<String>,
209 pub name: String,
210 pub label: String,
211 pub options: Vec<SelectOption>,
212 #[serde(default)]
213 pub required: bool,
214 #[serde(skip_serializing_if = "Option::is_none")]
215 pub error: Option<String>,
216}
217
218#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
219pub struct MultiSelect {
220 #[serde(skip_serializing_if = "Option::is_none")]
222 pub id: Option<String>,
223 pub name: String,
224 pub label: String,
225 pub options: Vec<SelectOption>,
226 #[serde(default)]
227 pub required: bool,
228}
229
230#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
231pub struct SelectOption {
232 pub label: String,
233 pub value: String,
234}
235
236#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
237pub struct Switch {
238 #[serde(skip_serializing_if = "Option::is_none")]
240 pub id: Option<String>,
241 pub name: String,
242 pub label: String,
243 #[serde(default)]
244 pub default_checked: bool,
245}
246
247#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
248pub struct DateInput {
249 #[serde(skip_serializing_if = "Option::is_none")]
251 pub id: Option<String>,
252 pub name: String,
253 pub label: String,
254 #[serde(default)]
255 pub required: bool,
256}
257
258#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
259pub struct Slider {
260 #[serde(skip_serializing_if = "Option::is_none")]
262 pub id: Option<String>,
263 pub name: String,
264 pub label: String,
265 pub min: f64,
266 pub max: f64,
267 pub step: Option<f64>,
268 pub default_value: Option<f64>,
269}
270
271#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
274pub struct Stack {
275 #[serde(skip_serializing_if = "Option::is_none")]
277 pub id: Option<String>,
278 pub direction: StackDirection,
279 pub children: Vec<Component>,
280 #[serde(default)]
281 pub gap: u8,
282}
283
284#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
285#[serde(rename_all = "snake_case")]
286pub enum StackDirection {
287 Horizontal,
288 Vertical,
289}
290
291#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
292pub struct Grid {
293 #[serde(skip_serializing_if = "Option::is_none")]
295 pub id: Option<String>,
296 pub columns: u8,
297 pub children: Vec<Component>,
298 #[serde(default)]
299 pub gap: u8,
300}
301
302#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
303pub struct Card {
304 #[serde(skip_serializing_if = "Option::is_none")]
306 pub id: Option<String>,
307 pub title: Option<String>,
308 pub description: Option<String>,
309 pub content: Vec<Component>,
310 pub footer: Option<Vec<Component>>,
311}
312
313#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
314pub struct Container {
315 #[serde(skip_serializing_if = "Option::is_none")]
317 pub id: Option<String>,
318 pub children: Vec<Component>,
319 #[serde(default)]
320 pub padding: u8,
321}
322
323#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
324pub struct Divider {
325 #[serde(skip_serializing_if = "Option::is_none")]
327 pub id: Option<String>,
328}
329
330#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
331pub struct Tabs {
332 #[serde(skip_serializing_if = "Option::is_none")]
334 pub id: Option<String>,
335 pub tabs: Vec<Tab>,
336}
337
338#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
339pub struct Tab {
340 pub label: String,
341 pub content: Vec<Component>,
342}
343
344#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
347pub struct Table {
348 #[serde(skip_serializing_if = "Option::is_none")]
350 pub id: Option<String>,
351 pub columns: Vec<TableColumn>,
352 pub data: Vec<HashMap<String, serde_json::Value>>,
353 #[serde(default)]
355 pub sortable: bool,
356 #[serde(skip_serializing_if = "Option::is_none")]
358 pub page_size: Option<u32>,
359 #[serde(default)]
361 pub striped: bool,
362}
363
364#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
365pub struct TableColumn {
366 pub header: String,
367 pub accessor_key: String,
368 #[serde(default = "default_true")]
370 pub sortable: bool,
371}
372
373fn default_true() -> bool {
374 true
375}
376
377#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
378pub struct List {
379 #[serde(skip_serializing_if = "Option::is_none")]
381 pub id: Option<String>,
382 pub items: Vec<String>,
383 #[serde(default)]
384 pub ordered: bool,
385}
386
387#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
388pub struct KeyValue {
389 #[serde(skip_serializing_if = "Option::is_none")]
391 pub id: Option<String>,
392 pub pairs: Vec<KeyValuePair>,
393}
394
395#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
396pub struct KeyValuePair {
397 pub key: String,
398 pub value: String,
399}
400
401#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
402pub struct CodeBlock {
403 #[serde(skip_serializing_if = "Option::is_none")]
405 pub id: Option<String>,
406 pub code: String,
407 pub language: Option<String>,
408}
409
410#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
413pub struct Chart {
414 #[serde(skip_serializing_if = "Option::is_none")]
416 pub id: Option<String>,
417 #[serde(skip_serializing_if = "Option::is_none")]
418 pub title: Option<String>,
419 pub kind: ChartKind,
420 pub data: Vec<HashMap<String, serde_json::Value>>,
421 pub x_key: String,
422 pub y_keys: Vec<String>,
423 #[serde(skip_serializing_if = "Option::is_none")]
425 pub x_label: Option<String>,
426 #[serde(skip_serializing_if = "Option::is_none")]
428 pub y_label: Option<String>,
429 #[serde(default = "default_show_legend")]
431 pub show_legend: bool,
432 #[serde(skip_serializing_if = "Option::is_none")]
434 pub colors: Option<Vec<String>>,
435}
436
437fn default_show_legend() -> bool {
438 true
439}
440
441#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
442#[serde(rename_all = "snake_case")]
443pub enum ChartKind {
444 Bar,
445 Line,
446 Area,
447 Pie,
448}
449
450#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
453pub struct Alert {
454 #[serde(skip_serializing_if = "Option::is_none")]
456 pub id: Option<String>,
457 pub title: String,
458 pub description: Option<String>,
459 #[serde(default)]
460 pub variant: AlertVariant,
461}
462
463#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
464#[serde(rename_all = "snake_case")]
465pub enum AlertVariant {
466 #[default]
467 Info,
468 Success,
469 Warning,
470 Error,
471}
472
473#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
474pub struct Progress {
475 #[serde(skip_serializing_if = "Option::is_none")]
477 pub id: Option<String>,
478 pub value: u8, pub label: Option<String>,
480}
481
482#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
483pub struct Toast {
484 #[serde(skip_serializing_if = "Option::is_none")]
485 pub id: Option<String>,
486 pub message: String,
487 #[serde(default)]
488 pub variant: AlertVariant,
489 #[serde(default = "default_toast_duration")]
491 pub duration: u32,
492 #[serde(default = "default_true")]
493 pub dismissible: bool,
494}
495
496fn default_toast_duration() -> u32 {
497 5000
498}
499
500#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
501pub struct Modal {
502 #[serde(skip_serializing_if = "Option::is_none")]
503 pub id: Option<String>,
504 pub title: String,
505 pub content: Vec<Component>,
506 pub footer: Option<Vec<Component>>,
507 #[serde(default)]
508 pub size: ModalSize,
509 #[serde(default = "default_true")]
510 pub closable: bool,
511}
512
513#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
514#[serde(rename_all = "snake_case")]
515pub enum ModalSize {
516 Small,
517 #[default]
518 Medium,
519 Large,
520 Full,
521}
522
523#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
524pub struct Spinner {
525 #[serde(skip_serializing_if = "Option::is_none")]
526 pub id: Option<String>,
527 #[serde(default)]
528 pub size: SpinnerSize,
529 pub label: Option<String>,
530}
531
532#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
533#[serde(rename_all = "snake_case")]
534pub enum SpinnerSize {
535 Small,
536 #[default]
537 Medium,
538 Large,
539}
540
541#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
542pub struct Skeleton {
543 #[serde(skip_serializing_if = "Option::is_none")]
544 pub id: Option<String>,
545 #[serde(default)]
546 pub variant: SkeletonVariant,
547 pub width: Option<String>,
548 pub height: Option<String>,
549}
550
551#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
552#[serde(rename_all = "snake_case")]
553pub enum SkeletonVariant {
554 #[default]
555 Text,
556 Circle,
557 Rectangle,
558}
559
560#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
561pub struct Textarea {
562 #[serde(skip_serializing_if = "Option::is_none")]
563 pub id: Option<String>,
564 pub name: String,
565 pub label: String,
566 pub placeholder: Option<String>,
567 #[serde(default = "default_textarea_rows")]
568 pub rows: u8,
569 #[serde(default)]
570 pub required: bool,
571 pub default_value: Option<String>,
572 #[serde(skip_serializing_if = "Option::is_none")]
573 pub error: Option<String>,
574}
575
576fn default_textarea_rows() -> u8 {
577 4
578}
579
580#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
584#[serde(rename_all = "snake_case")]
585pub enum Theme {
586 #[default]
587 Light,
588 Dark,
589 System,
590}
591
592#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
593pub struct UiResponse {
594 #[serde(default)]
596 pub id: Option<String>,
597 #[serde(default)]
599 pub theme: Theme,
600 pub components: Vec<Component>,
602}
603
604impl UiResponse {
605 pub fn new(components: Vec<Component>) -> Self {
606 Self { id: None, theme: Theme::default(), components }
607 }
608
609 pub fn with_theme(mut self, theme: Theme) -> Self {
610 self.theme = theme;
611 self
612 }
613
614 pub fn with_id(mut self, id: impl Into<String>) -> Self {
615 self.id = Some(id.into());
616 self
617 }
618
619 pub fn to_content(self) -> adk_core::Content {
620 let json = serde_json::to_vec(&self).unwrap_or_default();
621 adk_core::Content {
622 role: "model".to_string(),
623 parts: vec![adk_core::Part::InlineData {
624 mime_type: MIME_TYPE_UI.to_string(),
625 data: json,
626 }],
627 }
628 }
629}
630
631#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
635#[serde(tag = "action", rename_all = "snake_case")]
636pub enum UiEvent {
637 FormSubmit {
639 action_id: String,
641 data: HashMap<String, serde_json::Value>,
643 },
644 ButtonClick {
646 action_id: String,
648 },
649 InputChange {
651 name: String,
653 value: serde_json::Value,
655 },
656 TabChange {
658 index: usize,
660 },
661}
662
663impl UiEvent {
664 pub fn to_user_message(&self) -> String {
666 match self {
667 UiEvent::FormSubmit { action_id, data } => {
668 let json = serde_json::to_string_pretty(data).unwrap_or_default();
669 format!("[UI Event: Form submitted]\nAction: {}\nData:\n{}", action_id, json)
670 }
671 UiEvent::ButtonClick { action_id } => {
672 format!("[UI Event: Button clicked]\nAction: {}", action_id)
673 }
674 UiEvent::InputChange { name, value } => {
675 format!("[UI Event: Input changed]\nField: {}\nValue: {}", name, value)
676 }
677 UiEvent::TabChange { index } => {
678 format!("[UI Event: Tab changed]\nIndex: {}", index)
679 }
680 }
681 }
682
683 pub fn to_content(&self) -> adk_core::Content {
685 adk_core::Content::new("user").with_text(self.to_user_message())
686 }
687}
688
689#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
693#[serde(rename_all = "snake_case")]
694pub enum UiOperation {
695 Replace,
697 Patch,
699 Append,
701 Remove,
703}
704
705#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
710pub struct UiUpdate {
711 pub target_id: String,
713 pub operation: UiOperation,
715 #[serde(skip_serializing_if = "Option::is_none")]
717 pub payload: Option<Component>,
718}
719
720impl UiUpdate {
721 pub fn replace(target_id: impl Into<String>, component: Component) -> Self {
723 Self {
724 target_id: target_id.into(),
725 operation: UiOperation::Replace,
726 payload: Some(component),
727 }
728 }
729
730 pub fn remove(target_id: impl Into<String>) -> Self {
732 Self { target_id: target_id.into(), operation: UiOperation::Remove, payload: None }
733 }
734
735 pub fn append(target_id: impl Into<String>, component: Component) -> Self {
737 Self {
738 target_id: target_id.into(),
739 operation: UiOperation::Append,
740 payload: Some(component),
741 }
742 }
743
744 pub fn to_content(self) -> adk_core::Content {
746 let json = serde_json::to_vec(&self).unwrap_or_default();
747 adk_core::Content {
748 role: "model".to_string(),
749 parts: vec![adk_core::Part::InlineData {
750 mime_type: MIME_TYPE_UI_UPDATE.to_string(),
751 data: json,
752 }],
753 }
754 }
755}
756
757#[cfg(test)]
758mod tests {
759 use super::*;
760
761 #[test]
762 fn test_component_serialization_roundtrip() {
763 let text = Component::Text(Text {
764 id: Some("text-1".to_string()),
765 content: "Hello".to_string(),
766 variant: TextVariant::Body,
767 });
768
769 let json = serde_json::to_string(&text).unwrap();
770 let deserialized: Component = serde_json::from_str(&json).unwrap();
771
772 if let Component::Text(t) = deserialized {
773 assert_eq!(t.content, "Hello");
774 assert_eq!(t.id, Some("text-1".to_string()));
775 } else {
776 panic!("Expected Text component");
777 }
778 }
779
780 #[test]
781 fn test_ui_response_with_id() {
782 let ui = UiResponse::new(vec![]).with_id("response-123").with_theme(Theme::Dark);
783
784 assert_eq!(ui.id, Some("response-123".to_string()));
785 assert!(matches!(ui.theme, Theme::Dark));
786 }
787
788 #[test]
789 fn test_badge_variants_serialize() {
790 let badge = Badge { id: None, label: "Test".to_string(), variant: BadgeVariant::Success };
791 let json = serde_json::to_string(&badge).unwrap();
792 assert!(json.contains("success"));
793 }
794
795 #[test]
796 fn test_ui_event_to_message() {
797 let event = UiEvent::FormSubmit { action_id: "submit".to_string(), data: HashMap::new() };
798 let msg = event.to_user_message();
799 assert!(msg.contains("Form submitted"));
800 assert!(msg.contains("submit"));
801 }
802
803 #[test]
804 fn test_ui_update_replace() {
805 let update = UiUpdate::replace(
806 "target-1",
807 Component::Text(Text {
808 id: None,
809 content: "Updated".to_string(),
810 variant: TextVariant::Body,
811 }),
812 );
813
814 assert_eq!(update.target_id, "target-1");
815 assert!(matches!(update.operation, UiOperation::Replace));
816 assert!(update.payload.is_some());
817 }
818
819 #[test]
820 fn test_ui_update_remove() {
821 let update = UiUpdate::remove("to-delete");
822 assert_eq!(update.target_id, "to-delete");
823 assert!(matches!(update.operation, UiOperation::Remove));
824 assert!(update.payload.is_none());
825 }
826
827 #[test]
828 fn test_key_value_pairs() {
829 let kv = KeyValue {
830 id: Some("kv-1".to_string()),
831 pairs: vec![
832 KeyValuePair { key: "Name".to_string(), value: "Alice".to_string() },
833 KeyValuePair { key: "Age".to_string(), value: "30".to_string() },
834 ],
835 };
836
837 let json = serde_json::to_string(&kv).unwrap();
838 assert!(json.contains("pairs"));
839 assert!(json.contains("Alice"));
840 }
841
842 #[test]
843 fn test_component_with_id_skips_none() {
844 let text = Component::Text(Text {
845 id: None,
846 content: "No ID".to_string(),
847 variant: TextVariant::Body,
848 });
849
850 let json = serde_json::to_string(&text).unwrap();
851 assert!(!json.contains("\"id\""));
853 }
854}