1use crate::surface_runtime::SurfaceRef;
2use schemars::JsonSchema;
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6pub const MIME_TYPE_UI: &str = "application/vnd.adk.ui+json";
7pub const MIME_TYPE_UI_UPDATE: &str = "application/vnd.adk.ui.update+json";
8pub const MIME_TYPE_UI_EVENT: &str = "application/vnd.adk.ui.event+json";
9
10#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
11#[serde(tag = "type", rename_all = "snake_case")]
12pub enum Component {
13 Text(Text),
15 Button(Button),
16 Icon(Icon),
17 Image(Image),
18 Badge(Badge),
19
20 TextInput(TextInput),
22 NumberInput(NumberInput),
23 Select(Select),
24 MultiSelect(MultiSelect),
25 Switch(Switch),
26 DateInput(DateInput),
27 Slider(Slider),
28
29 Stack(Stack),
31 Grid(Grid),
32 Card(Card),
33 Container(Container),
34 Divider(Divider),
35 Tabs(Tabs),
36
37 Table(Table),
39 List(List),
40 KeyValue(KeyValue),
41 CodeBlock(CodeBlock),
42
43 Chart(Chart),
45 Scene3d(Scene3d),
46
47 Alert(Alert),
49 Progress(Progress),
50 Toast(Toast),
51 Modal(Modal),
52 Spinner(Spinner),
53 Skeleton(Skeleton),
54
55 Textarea(Textarea),
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
62pub struct Text {
63 #[serde(skip_serializing_if = "Option::is_none")]
65 pub id: Option<String>,
66 pub content: String,
67 #[serde(default)]
68 pub variant: TextVariant,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
72#[serde(rename_all = "snake_case")]
73pub enum TextVariant {
74 H1,
75 H2,
76 H3,
77 H4,
78 #[default]
79 Body,
80 Caption,
81 Code,
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
85pub struct Button {
86 #[serde(skip_serializing_if = "Option::is_none")]
88 pub id: Option<String>,
89 pub label: String,
90 pub action_id: String,
91 #[serde(default)]
92 pub variant: ButtonVariant,
93 #[serde(default)]
94 pub disabled: bool,
95 #[serde(skip_serializing_if = "Option::is_none")]
97 pub icon: Option<String>,
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
101#[serde(rename_all = "snake_case")]
102pub enum ButtonVariant {
103 #[default]
104 Primary,
105 Secondary,
106 Danger,
107 Ghost,
108 Outline,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
112pub struct Icon {
113 #[serde(skip_serializing_if = "Option::is_none")]
115 pub id: Option<String>,
116 pub name: String, #[serde(default)]
118 pub size: u8,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
122pub struct Image {
123 #[serde(skip_serializing_if = "Option::is_none")]
125 pub id: Option<String>,
126 pub src: String,
127 pub alt: Option<String>,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
131pub struct Badge {
132 #[serde(skip_serializing_if = "Option::is_none")]
134 pub id: Option<String>,
135 pub label: String,
136 #[serde(default)]
137 pub variant: BadgeVariant,
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
141#[serde(rename_all = "snake_case")]
142pub enum BadgeVariant {
143 #[default]
144 Default,
145 Info,
146 Success,
147 Warning,
148 Error,
149 Secondary,
150 Outline,
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
156pub struct TextInput {
157 #[serde(skip_serializing_if = "Option::is_none")]
159 pub id: Option<String>,
160 pub name: String,
161 pub label: String,
162 #[serde(default = "default_input_type")]
164 pub input_type: String,
165 #[serde(skip_serializing_if = "Option::is_none")]
166 pub placeholder: Option<String>,
167 #[serde(default)]
168 pub required: bool,
169 #[serde(skip_serializing_if = "Option::is_none")]
170 pub default_value: Option<String>,
171 #[serde(skip_serializing_if = "Option::is_none")]
173 pub min_length: Option<usize>,
174 #[serde(skip_serializing_if = "Option::is_none")]
176 pub max_length: Option<usize>,
177 #[serde(skip_serializing_if = "Option::is_none")]
178 pub error: Option<String>,
179}
180
181fn default_input_type() -> String {
182 "text".to_string()
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
186pub struct NumberInput {
187 #[serde(skip_serializing_if = "Option::is_none")]
189 pub id: Option<String>,
190 pub name: String,
191 pub label: String,
192 #[serde(skip_serializing_if = "Option::is_none")]
193 pub min: Option<f64>,
194 #[serde(skip_serializing_if = "Option::is_none")]
195 pub max: Option<f64>,
196 #[serde(skip_serializing_if = "Option::is_none")]
197 pub step: Option<f64>,
198 #[serde(default)]
199 pub required: bool,
200 #[serde(skip_serializing_if = "Option::is_none")]
202 pub default_value: Option<f64>,
203 #[serde(skip_serializing_if = "Option::is_none")]
204 pub error: Option<String>,
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
208pub struct Select {
209 #[serde(skip_serializing_if = "Option::is_none")]
211 pub id: Option<String>,
212 pub name: String,
213 pub label: String,
214 pub options: Vec<SelectOption>,
215 #[serde(default)]
216 pub required: bool,
217 #[serde(skip_serializing_if = "Option::is_none")]
218 pub error: Option<String>,
219}
220
221#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
222pub struct MultiSelect {
223 #[serde(skip_serializing_if = "Option::is_none")]
225 pub id: Option<String>,
226 pub name: String,
227 pub label: String,
228 pub options: Vec<SelectOption>,
229 #[serde(default)]
230 pub required: bool,
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
234pub struct SelectOption {
235 pub label: String,
236 pub value: String,
237}
238
239#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
240pub struct Switch {
241 #[serde(skip_serializing_if = "Option::is_none")]
243 pub id: Option<String>,
244 pub name: String,
245 pub label: String,
246 #[serde(default)]
247 pub default_checked: bool,
248}
249
250#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
251pub struct DateInput {
252 #[serde(skip_serializing_if = "Option::is_none")]
254 pub id: Option<String>,
255 pub name: String,
256 pub label: String,
257 #[serde(default)]
258 pub required: bool,
259}
260
261#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
262pub struct Slider {
263 #[serde(skip_serializing_if = "Option::is_none")]
265 pub id: Option<String>,
266 pub name: String,
267 pub label: String,
268 pub min: f64,
269 pub max: f64,
270 pub step: Option<f64>,
271 pub default_value: Option<f64>,
272}
273
274#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
277pub struct Stack {
278 #[serde(skip_serializing_if = "Option::is_none")]
280 pub id: Option<String>,
281 pub direction: StackDirection,
282 pub children: Vec<Component>,
283 #[serde(default)]
284 pub gap: u8,
285}
286
287#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
288#[serde(rename_all = "snake_case")]
289pub enum StackDirection {
290 Horizontal,
291 Vertical,
292}
293
294#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
295pub struct Grid {
296 #[serde(skip_serializing_if = "Option::is_none")]
298 pub id: Option<String>,
299 pub columns: u8,
300 pub children: Vec<Component>,
301 #[serde(default)]
302 pub gap: u8,
303}
304
305#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
306pub struct Card {
307 #[serde(skip_serializing_if = "Option::is_none")]
309 pub id: Option<String>,
310 pub title: Option<String>,
311 pub description: Option<String>,
312 pub content: Vec<Component>,
313 pub footer: Option<Vec<Component>>,
314}
315
316#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
317pub struct Container {
318 #[serde(skip_serializing_if = "Option::is_none")]
320 pub id: Option<String>,
321 pub children: Vec<Component>,
322 #[serde(default)]
323 pub padding: u8,
324}
325
326#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
327pub struct Divider {
328 #[serde(skip_serializing_if = "Option::is_none")]
330 pub id: Option<String>,
331}
332
333#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
334pub struct Tabs {
335 #[serde(skip_serializing_if = "Option::is_none")]
337 pub id: Option<String>,
338 pub tabs: Vec<Tab>,
339}
340
341#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
342pub struct Tab {
343 pub label: String,
344 pub content: Vec<Component>,
345}
346
347#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
351pub struct DataSource {
352 pub binding_path: String,
354 #[serde(default)]
355 pub refresh: DataRefresh,
356 #[serde(default, skip_serializing_if = "Option::is_none")]
358 pub ttl: Option<u64>,
359}
360
361#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
362#[serde(tag = "mode", rename_all = "snake_case")]
363pub enum DataRefresh {
364 #[default]
365 Manual,
366 Interval {
367 secs: u64,
368 },
369 Push,
370}
371
372#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
373pub struct Table {
374 #[serde(skip_serializing_if = "Option::is_none")]
376 pub id: Option<String>,
377 pub columns: Vec<TableColumn>,
378 #[serde(default)]
379 pub data: Vec<HashMap<String, serde_json::Value>>,
380 #[serde(default, skip_serializing_if = "Option::is_none")]
381 pub data_source: Option<DataSource>,
382 #[serde(default)]
384 pub sortable: bool,
385 #[serde(skip_serializing_if = "Option::is_none")]
387 pub page_size: Option<u32>,
388 #[serde(default)]
390 pub striped: bool,
391}
392
393#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
394pub struct TableColumn {
395 pub header: String,
396 pub accessor_key: String,
397 #[serde(default = "default_true")]
399 pub sortable: bool,
400}
401
402fn default_true() -> bool {
403 true
404}
405
406#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
407pub struct List {
408 #[serde(skip_serializing_if = "Option::is_none")]
410 pub id: Option<String>,
411 pub items: Vec<String>,
412 #[serde(default)]
413 pub ordered: bool,
414}
415
416#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
417pub struct KeyValue {
418 #[serde(skip_serializing_if = "Option::is_none")]
420 pub id: Option<String>,
421 #[serde(default)]
422 pub pairs: Vec<KeyValuePair>,
423 #[serde(default, skip_serializing_if = "Option::is_none")]
424 pub data_source: Option<DataSource>,
425}
426
427#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
428pub struct KeyValuePair {
429 pub key: String,
430 pub value: String,
431}
432
433#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
434pub struct CodeBlock {
435 #[serde(skip_serializing_if = "Option::is_none")]
437 pub id: Option<String>,
438 pub code: String,
439 pub language: Option<String>,
440}
441
442#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
445pub struct Chart {
446 #[serde(skip_serializing_if = "Option::is_none")]
448 pub id: Option<String>,
449 #[serde(skip_serializing_if = "Option::is_none")]
450 pub title: Option<String>,
451 pub kind: ChartKind,
452 #[serde(default)]
453 pub data: Vec<HashMap<String, serde_json::Value>>,
454 #[serde(default, skip_serializing_if = "Option::is_none")]
455 pub data_source: Option<DataSource>,
456 pub x_key: String,
457 pub y_keys: Vec<String>,
458 #[serde(default)]
459 pub x_type: ChartXType,
460 #[serde(default, skip_serializing_if = "Option::is_none")]
461 pub time_format: Option<String>,
462 #[serde(default, skip_serializing_if = "Option::is_none")]
463 pub window: Option<u32>,
464 #[serde(skip_serializing_if = "Option::is_none")]
466 pub x_label: Option<String>,
467 #[serde(skip_serializing_if = "Option::is_none")]
469 pub y_label: Option<String>,
470 #[serde(default = "default_show_legend")]
472 pub show_legend: bool,
473 #[serde(skip_serializing_if = "Option::is_none")]
475 pub colors: Option<Vec<String>>,
476}
477
478fn default_show_legend() -> bool {
479 true
480}
481
482#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
483#[serde(rename_all = "snake_case")]
484pub enum ChartKind {
485 Bar,
486 Line,
487 Area,
488 Pie,
489 Scatter,
490 Sparkline,
491}
492
493#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
494#[serde(rename_all = "snake_case")]
495pub enum ChartXType {
496 #[default]
497 Category,
498 Numeric,
499 Time,
500}
501
502#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
505pub struct Scene3d {
506 #[serde(skip_serializing_if = "Option::is_none")]
507 pub id: Option<String>,
508 #[serde(skip_serializing_if = "Option::is_none")]
509 pub title: Option<String>,
510 #[serde(skip_serializing_if = "Option::is_none")]
511 pub description: Option<String>,
512 #[serde(default = "default_scene_height")]
513 pub height: u16,
514 #[serde(default)]
515 pub background: SceneBackground,
516 #[serde(default)]
517 pub camera: SceneCamera,
518 pub objects: Vec<SceneObject>,
519 #[serde(default)]
520 pub auto_rotate: bool,
521 #[serde(default = "default_true")]
522 pub controls: bool,
523 #[serde(default, skip_serializing_if = "Option::is_none")]
524 pub fallback: Option<String>,
525}
526
527fn default_scene_height() -> u16 {
528 440
529}
530
531#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
532#[serde(rename_all = "snake_case")]
533pub enum SceneBackground {
534 Transparent,
535 #[default]
536 Surface,
537 Muted,
538 Contrast,
539}
540
541#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
542pub struct SceneCamera {
543 #[serde(default = "default_camera_position")]
544 pub position: [f32; 3],
545 #[serde(default = "default_camera_target")]
546 pub target: [f32; 3],
547 #[serde(default = "default_camera_fov")]
548 pub fov: f32,
549}
550
551impl Default for SceneCamera {
552 fn default() -> Self {
553 Self {
554 position: default_camera_position(),
555 target: default_camera_target(),
556 fov: default_camera_fov(),
557 }
558 }
559}
560
561fn default_camera_position() -> [f32; 3] {
562 [4.5, 3.0, 6.0]
563}
564fn default_camera_target() -> [f32; 3] {
565 [0.0, 0.0, 0.0]
566}
567fn default_camera_fov() -> f32 {
568 42.0
569}
570
571#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
572#[serde(tag = "kind", rename_all = "snake_case")]
573pub enum SceneObject {
574 Primitive(ScenePrimitive),
575 Model(SceneModel),
576}
577
578#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
579pub struct ScenePrimitive {
580 pub id: String,
581 pub shape: ScenePrimitiveShape,
582 #[serde(default)]
583 pub position: [f32; 3],
584 #[serde(default)]
585 pub rotation: [f32; 3],
586 #[serde(default = "default_scene_scale")]
587 pub scale: [f32; 3],
588 #[serde(default)]
589 pub material: SceneMaterialRole,
590 #[serde(default, skip_serializing_if = "Option::is_none")]
591 pub label: Option<String>,
592 #[serde(default, skip_serializing_if = "Option::is_none")]
593 pub action_id: Option<String>,
594}
595
596fn default_scene_scale() -> [f32; 3] {
597 [1.0, 1.0, 1.0]
598}
599
600#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
601#[serde(rename_all = "snake_case")]
602pub enum ScenePrimitiveShape {
603 Box,
604 Sphere,
605 Cylinder,
606 Torus,
607 Plane,
608}
609
610#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
611#[serde(rename_all = "snake_case")]
612pub enum SceneMaterialRole {
613 #[default]
614 Primary,
615 Secondary,
616 Surface,
617 Muted,
618 Success,
619 Warning,
620 Info,
621}
622
623#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
624pub struct SceneModel {
625 pub id: String,
626 pub asset_id: String,
628 #[serde(default)]
629 pub position: [f32; 3],
630 #[serde(default)]
631 pub rotation: [f32; 3],
632 #[serde(default = "default_scene_scale")]
633 pub scale: [f32; 3],
634 #[serde(default, skip_serializing_if = "Option::is_none")]
635 pub label: Option<String>,
636 #[serde(default, skip_serializing_if = "Option::is_none")]
637 pub action_id: Option<String>,
638}
639
640#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
643pub struct Alert {
644 #[serde(skip_serializing_if = "Option::is_none")]
646 pub id: Option<String>,
647 pub title: String,
648 pub description: Option<String>,
649 #[serde(default)]
650 pub variant: AlertVariant,
651}
652
653#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
654#[serde(rename_all = "snake_case")]
655pub enum AlertVariant {
656 #[default]
657 Info,
658 Success,
659 Warning,
660 Error,
661}
662
663#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
664pub struct Progress {
665 #[serde(skip_serializing_if = "Option::is_none")]
667 pub id: Option<String>,
668 pub value: u8, pub label: Option<String>,
670}
671
672#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
673pub struct Toast {
674 #[serde(skip_serializing_if = "Option::is_none")]
675 pub id: Option<String>,
676 pub message: String,
677 #[serde(default)]
678 pub variant: AlertVariant,
679 #[serde(default = "default_toast_duration")]
681 pub duration: u32,
682 #[serde(default = "default_true")]
683 pub dismissible: bool,
684}
685
686fn default_toast_duration() -> u32 {
687 5000
688}
689
690#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
691pub struct Modal {
692 #[serde(skip_serializing_if = "Option::is_none")]
693 pub id: Option<String>,
694 pub title: String,
695 pub content: Vec<Component>,
696 pub footer: Option<Vec<Component>>,
697 #[serde(default)]
698 pub size: ModalSize,
699 #[serde(default = "default_true")]
700 pub closable: bool,
701}
702
703#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
704#[serde(rename_all = "snake_case")]
705pub enum ModalSize {
706 Small,
707 #[default]
708 Medium,
709 Large,
710 Full,
711}
712
713#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
714pub struct Spinner {
715 #[serde(skip_serializing_if = "Option::is_none")]
716 pub id: Option<String>,
717 #[serde(default)]
718 pub size: SpinnerSize,
719 pub label: Option<String>,
720}
721
722#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
723#[serde(rename_all = "snake_case")]
724pub enum SpinnerSize {
725 Small,
726 #[default]
727 Medium,
728 Large,
729}
730
731#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
732pub struct Skeleton {
733 #[serde(skip_serializing_if = "Option::is_none")]
734 pub id: Option<String>,
735 #[serde(default)]
736 pub variant: SkeletonVariant,
737 pub width: Option<String>,
738 pub height: Option<String>,
739}
740
741#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
742#[serde(rename_all = "snake_case")]
743pub enum SkeletonVariant {
744 #[default]
745 Text,
746 Circle,
747 Rectangle,
748}
749
750#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
751pub struct Textarea {
752 #[serde(skip_serializing_if = "Option::is_none")]
753 pub id: Option<String>,
754 pub name: String,
755 pub label: String,
756 pub placeholder: Option<String>,
757 #[serde(default = "default_textarea_rows")]
758 pub rows: u8,
759 #[serde(default)]
760 pub required: bool,
761 pub default_value: Option<String>,
762 #[serde(skip_serializing_if = "Option::is_none")]
763 pub error: Option<String>,
764}
765
766fn default_textarea_rows() -> u8 {
767 4
768}
769
770#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
774#[serde(rename_all = "snake_case")]
775pub enum Theme {
776 #[default]
777 Light,
778 Dark,
779 System,
780}
781
782#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
783pub struct UiResponse {
784 #[serde(default)]
786 pub id: Option<String>,
787 #[serde(default, skip_serializing_if = "Option::is_none")]
789 pub kit_id: Option<String>,
790 #[serde(default)]
792 pub theme: Theme,
793 pub components: Vec<Component>,
795}
796
797impl UiResponse {
798 pub fn new(components: Vec<Component>) -> Self {
799 Self {
800 id: None,
801 kit_id: None,
802 theme: Theme::default(),
803 components,
804 }
805 }
806
807 pub fn with_theme(mut self, theme: Theme) -> Self {
808 self.theme = theme;
809 self
810 }
811
812 pub fn with_id(mut self, id: impl Into<String>) -> Self {
813 self.id = Some(id.into());
814 self
815 }
816
817 pub fn with_kit_id(mut self, kit_id: impl Into<String>) -> Self {
818 self.kit_id = Some(kit_id.into());
819 self
820 }
821
822 pub fn to_content(self) -> crate::compat::Content {
823 let json = serde_json::to_vec(&self).unwrap_or_default();
824 crate::compat::Content {
825 role: "model".to_string(),
826 parts: vec![crate::compat::inline_data_part(MIME_TYPE_UI, json)],
827 }
828 }
829}
830
831#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
835#[serde(tag = "action", rename_all = "snake_case")]
836pub enum UiEvent {
837 FormSubmit {
839 action_id: String,
841 data: HashMap<String, serde_json::Value>,
843 },
844 ButtonClick {
846 action_id: String,
848 },
849 InputChange {
851 name: String,
853 value: serde_json::Value,
855 },
856 TabChange {
858 index: usize,
860 },
861}
862
863#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
865pub struct UiEventEnvelope {
866 pub surface_ref: SurfaceRef,
867 pub event: UiEvent,
868 pub occurred_at: String,
869 #[serde(default, skip_serializing_if = "Option::is_none")]
870 pub metadata: Option<serde_json::Value>,
871}
872
873impl UiEventEnvelope {
874 pub fn new(surface_ref: SurfaceRef, event: UiEvent) -> Self {
875 Self {
876 surface_ref,
877 event,
878 occurred_at: chrono::Utc::now().to_rfc3339(),
879 metadata: None,
880 }
881 }
882
883 pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
884 self.metadata = Some(metadata);
885 self
886 }
887
888 pub fn to_content(&self) -> crate::compat::Content {
889 let json = serde_json::to_vec(self).unwrap_or_default();
890 crate::compat::Content {
891 role: "user".to_string(),
892 parts: vec![crate::compat::inline_data_part(MIME_TYPE_UI_EVENT, json)],
893 }
894 }
895}
896
897impl UiEvent {
898 pub fn to_user_message(&self) -> String {
900 match self {
901 UiEvent::FormSubmit { action_id, data } => {
902 let json = serde_json::to_string_pretty(data).unwrap_or_default();
903 format!(
904 "[UI Event: Form submitted]\nAction: {}\nData:\n{}",
905 action_id, json
906 )
907 }
908 UiEvent::ButtonClick { action_id } => {
909 format!("[UI Event: Button clicked]\nAction: {}", action_id)
910 }
911 UiEvent::InputChange { name, value } => {
912 format!(
913 "[UI Event: Input changed]\nField: {}\nValue: {}",
914 name, value
915 )
916 }
917 UiEvent::TabChange { index } => {
918 format!("[UI Event: Tab changed]\nIndex: {}", index)
919 }
920 }
921 }
922
923 pub fn to_content(&self) -> crate::compat::Content {
925 let json = serde_json::to_vec(self).unwrap_or_default();
926 crate::compat::Content {
927 role: "user".to_string(),
928 parts: vec![crate::compat::inline_data_part(MIME_TYPE_UI_EVENT, json)],
929 }
930 }
931}
932
933#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
937#[serde(rename_all = "snake_case")]
938pub enum UiOperation {
939 Replace,
941 Patch,
943 Append,
945 Remove,
947}
948
949#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
954pub struct UiUpdate {
955 pub target_id: String,
957 pub operation: UiOperation,
959 #[serde(skip_serializing_if = "Option::is_none")]
961 pub payload: Option<Component>,
962}
963
964impl UiUpdate {
965 pub fn replace(target_id: impl Into<String>, component: Component) -> Self {
967 Self {
968 target_id: target_id.into(),
969 operation: UiOperation::Replace,
970 payload: Some(component),
971 }
972 }
973
974 pub fn patch(target_id: impl Into<String>, component: Component) -> Self {
976 Self {
977 target_id: target_id.into(),
978 operation: UiOperation::Patch,
979 payload: Some(component),
980 }
981 }
982
983 pub fn remove(target_id: impl Into<String>) -> Self {
985 Self {
986 target_id: target_id.into(),
987 operation: UiOperation::Remove,
988 payload: None,
989 }
990 }
991
992 pub fn append(target_id: impl Into<String>, component: Component) -> Self {
994 Self {
995 target_id: target_id.into(),
996 operation: UiOperation::Append,
997 payload: Some(component),
998 }
999 }
1000
1001 pub fn to_content(self) -> crate::compat::Content {
1003 let json = serde_json::to_vec(&self).unwrap_or_default();
1004 crate::compat::Content {
1005 role: "model".to_string(),
1006 parts: vec![crate::compat::inline_data_part(MIME_TYPE_UI_UPDATE, json)],
1007 }
1008 }
1009}
1010
1011#[cfg(test)]
1012mod tests {
1013 use super::*;
1014
1015 #[test]
1016 fn test_component_serialization_roundtrip() {
1017 let text = Component::Text(Text {
1018 id: Some("text-1".to_string()),
1019 content: "Hello".to_string(),
1020 variant: TextVariant::Body,
1021 });
1022
1023 let json = serde_json::to_string(&text).unwrap();
1024 let deserialized: Component = serde_json::from_str(&json).unwrap();
1025
1026 if let Component::Text(t) = deserialized {
1027 assert_eq!(t.content, "Hello");
1028 assert_eq!(t.id, Some("text-1".to_string()));
1029 } else {
1030 panic!("Expected Text component");
1031 }
1032 }
1033
1034 #[test]
1035 fn test_ui_response_with_id() {
1036 let ui = UiResponse::new(vec![])
1037 .with_id("response-123")
1038 .with_theme(Theme::Dark);
1039
1040 assert_eq!(ui.id, Some("response-123".to_string()));
1041 assert!(matches!(ui.theme, Theme::Dark));
1042 }
1043
1044 #[test]
1045 fn test_badge_variants_serialize() {
1046 let badge = Badge {
1047 id: None,
1048 label: "Test".to_string(),
1049 variant: BadgeVariant::Success,
1050 };
1051 let json = serde_json::to_string(&badge).unwrap();
1052 assert!(json.contains("success"));
1053 }
1054
1055 #[test]
1056 fn test_ui_event_to_message() {
1057 let event = UiEvent::FormSubmit {
1058 action_id: "submit".to_string(),
1059 data: HashMap::new(),
1060 };
1061 let msg = event.to_user_message();
1062 assert!(msg.contains("Form submitted"));
1063 assert!(msg.contains("submit"));
1064 }
1065
1066 #[test]
1067 fn ui_event_content_is_typed() {
1068 let event = UiEvent::ButtonClick {
1069 action_id: "open".to_string(),
1070 };
1071 let content = event.to_content();
1072 let value = serde_json::to_value(content).unwrap();
1073 assert_eq!(value["parts"][0]["mime_type"], MIME_TYPE_UI_EVENT);
1074 }
1075
1076 #[test]
1077 fn patch_constructor_emits_patch_operation() {
1078 let update = UiUpdate::patch(
1079 "status",
1080 Component::Text(Text {
1081 id: Some("status".to_string()),
1082 content: "ready".to_string(),
1083 variant: TextVariant::Body,
1084 }),
1085 );
1086 assert!(matches!(update.operation, UiOperation::Patch));
1087 }
1088
1089 #[test]
1090 fn test_ui_update_replace() {
1091 let update = UiUpdate::replace(
1092 "target-1",
1093 Component::Text(Text {
1094 id: None,
1095 content: "Updated".to_string(),
1096 variant: TextVariant::Body,
1097 }),
1098 );
1099
1100 assert_eq!(update.target_id, "target-1");
1101 assert!(matches!(update.operation, UiOperation::Replace));
1102 assert!(update.payload.is_some());
1103 }
1104
1105 #[test]
1106 fn test_ui_update_remove() {
1107 let update = UiUpdate::remove("to-delete");
1108 assert_eq!(update.target_id, "to-delete");
1109 assert!(matches!(update.operation, UiOperation::Remove));
1110 assert!(update.payload.is_none());
1111 }
1112
1113 #[test]
1114 fn test_key_value_pairs() {
1115 let kv = KeyValue {
1116 id: Some("kv-1".to_string()),
1117 pairs: vec![
1118 KeyValuePair {
1119 key: "Name".to_string(),
1120 value: "Alice".to_string(),
1121 },
1122 KeyValuePair {
1123 key: "Age".to_string(),
1124 value: "30".to_string(),
1125 },
1126 ],
1127 data_source: None,
1128 };
1129
1130 let json = serde_json::to_string(&kv).unwrap();
1131 assert!(json.contains("pairs"));
1132 assert!(json.contains("Alice"));
1133 }
1134
1135 #[test]
1136 fn test_component_with_id_skips_none() {
1137 let text = Component::Text(Text {
1138 id: None,
1139 content: "No ID".to_string(),
1140 variant: TextVariant::Body,
1141 });
1142
1143 let json = serde_json::to_string(&text).unwrap();
1144 assert!(!json.contains("\"id\""));
1146 }
1147}