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 Scene3d(Scene3d),
44
45 Alert(Alert),
47 Progress(Progress),
48 Toast(Toast),
49 Modal(Modal),
50 Spinner(Spinner),
51 Skeleton(Skeleton),
52
53 Textarea(Textarea),
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
60pub struct Text {
61 #[serde(skip_serializing_if = "Option::is_none")]
63 pub id: Option<String>,
64 pub content: String,
65 #[serde(default)]
66 pub variant: TextVariant,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
70#[serde(rename_all = "snake_case")]
71pub enum TextVariant {
72 H1,
73 H2,
74 H3,
75 H4,
76 #[default]
77 Body,
78 Caption,
79 Code,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
83pub struct Button {
84 #[serde(skip_serializing_if = "Option::is_none")]
86 pub id: Option<String>,
87 pub label: String,
88 pub action_id: String,
89 #[serde(default)]
90 pub variant: ButtonVariant,
91 #[serde(default)]
92 pub disabled: bool,
93 #[serde(skip_serializing_if = "Option::is_none")]
95 pub icon: Option<String>,
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
99#[serde(rename_all = "snake_case")]
100pub enum ButtonVariant {
101 #[default]
102 Primary,
103 Secondary,
104 Danger,
105 Ghost,
106 Outline,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
110pub struct Icon {
111 #[serde(skip_serializing_if = "Option::is_none")]
113 pub id: Option<String>,
114 pub name: String, #[serde(default)]
116 pub size: u8,
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
120pub struct Image {
121 #[serde(skip_serializing_if = "Option::is_none")]
123 pub id: Option<String>,
124 pub src: String,
125 pub alt: Option<String>,
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
129pub struct Badge {
130 #[serde(skip_serializing_if = "Option::is_none")]
132 pub id: Option<String>,
133 pub label: String,
134 #[serde(default)]
135 pub variant: BadgeVariant,
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
139#[serde(rename_all = "snake_case")]
140pub enum BadgeVariant {
141 #[default]
142 Default,
143 Info,
144 Success,
145 Warning,
146 Error,
147 Secondary,
148 Outline,
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
154pub struct TextInput {
155 #[serde(skip_serializing_if = "Option::is_none")]
157 pub id: Option<String>,
158 pub name: String,
159 pub label: String,
160 #[serde(default = "default_input_type")]
162 pub input_type: String,
163 #[serde(skip_serializing_if = "Option::is_none")]
164 pub placeholder: Option<String>,
165 #[serde(default)]
166 pub required: bool,
167 #[serde(skip_serializing_if = "Option::is_none")]
168 pub default_value: Option<String>,
169 #[serde(skip_serializing_if = "Option::is_none")]
171 pub min_length: Option<usize>,
172 #[serde(skip_serializing_if = "Option::is_none")]
174 pub max_length: Option<usize>,
175 #[serde(skip_serializing_if = "Option::is_none")]
176 pub error: Option<String>,
177}
178
179fn default_input_type() -> String {
180 "text".to_string()
181}
182
183#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
184pub struct NumberInput {
185 #[serde(skip_serializing_if = "Option::is_none")]
187 pub id: Option<String>,
188 pub name: String,
189 pub label: String,
190 #[serde(skip_serializing_if = "Option::is_none")]
191 pub min: Option<f64>,
192 #[serde(skip_serializing_if = "Option::is_none")]
193 pub max: Option<f64>,
194 #[serde(skip_serializing_if = "Option::is_none")]
195 pub step: Option<f64>,
196 #[serde(default)]
197 pub required: bool,
198 #[serde(skip_serializing_if = "Option::is_none")]
200 pub default_value: Option<f64>,
201 #[serde(skip_serializing_if = "Option::is_none")]
202 pub error: Option<String>,
203}
204
205#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
206pub struct Select {
207 #[serde(skip_serializing_if = "Option::is_none")]
209 pub id: Option<String>,
210 pub name: String,
211 pub label: String,
212 pub options: Vec<SelectOption>,
213 #[serde(default)]
214 pub required: bool,
215 #[serde(skip_serializing_if = "Option::is_none")]
216 pub error: Option<String>,
217}
218
219#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
220pub struct MultiSelect {
221 #[serde(skip_serializing_if = "Option::is_none")]
223 pub id: Option<String>,
224 pub name: String,
225 pub label: String,
226 pub options: Vec<SelectOption>,
227 #[serde(default)]
228 pub required: bool,
229}
230
231#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
232pub struct SelectOption {
233 pub label: String,
234 pub value: String,
235}
236
237#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
238pub struct Switch {
239 #[serde(skip_serializing_if = "Option::is_none")]
241 pub id: Option<String>,
242 pub name: String,
243 pub label: String,
244 #[serde(default)]
245 pub default_checked: bool,
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
249pub struct DateInput {
250 #[serde(skip_serializing_if = "Option::is_none")]
252 pub id: Option<String>,
253 pub name: String,
254 pub label: String,
255 #[serde(default)]
256 pub required: bool,
257}
258
259#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
260pub struct Slider {
261 #[serde(skip_serializing_if = "Option::is_none")]
263 pub id: Option<String>,
264 pub name: String,
265 pub label: String,
266 pub min: f64,
267 pub max: f64,
268 pub step: Option<f64>,
269 pub default_value: Option<f64>,
270}
271
272#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
275pub struct Stack {
276 #[serde(skip_serializing_if = "Option::is_none")]
278 pub id: Option<String>,
279 pub direction: StackDirection,
280 pub children: Vec<Component>,
281 #[serde(default)]
282 pub gap: u8,
283}
284
285#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
286#[serde(rename_all = "snake_case")]
287pub enum StackDirection {
288 Horizontal,
289 Vertical,
290}
291
292#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
293pub struct Grid {
294 #[serde(skip_serializing_if = "Option::is_none")]
296 pub id: Option<String>,
297 pub columns: u8,
298 pub children: Vec<Component>,
299 #[serde(default)]
300 pub gap: u8,
301}
302
303#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
304pub struct Card {
305 #[serde(skip_serializing_if = "Option::is_none")]
307 pub id: Option<String>,
308 pub title: Option<String>,
309 pub description: Option<String>,
310 pub content: Vec<Component>,
311 pub footer: Option<Vec<Component>>,
312}
313
314#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
315pub struct Container {
316 #[serde(skip_serializing_if = "Option::is_none")]
318 pub id: Option<String>,
319 pub children: Vec<Component>,
320 #[serde(default)]
321 pub padding: u8,
322}
323
324#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
325pub struct Divider {
326 #[serde(skip_serializing_if = "Option::is_none")]
328 pub id: Option<String>,
329}
330
331#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
332pub struct Tabs {
333 #[serde(skip_serializing_if = "Option::is_none")]
335 pub id: Option<String>,
336 pub tabs: Vec<Tab>,
337}
338
339#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
340pub struct Tab {
341 pub label: String,
342 pub content: Vec<Component>,
343}
344
345#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
348pub struct Table {
349 #[serde(skip_serializing_if = "Option::is_none")]
351 pub id: Option<String>,
352 pub columns: Vec<TableColumn>,
353 pub data: Vec<HashMap<String, serde_json::Value>>,
354 #[serde(default)]
356 pub sortable: bool,
357 #[serde(skip_serializing_if = "Option::is_none")]
359 pub page_size: Option<u32>,
360 #[serde(default)]
362 pub striped: bool,
363}
364
365#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
366pub struct TableColumn {
367 pub header: String,
368 pub accessor_key: String,
369 #[serde(default = "default_true")]
371 pub sortable: bool,
372}
373
374fn default_true() -> bool {
375 true
376}
377
378#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
379pub struct List {
380 #[serde(skip_serializing_if = "Option::is_none")]
382 pub id: Option<String>,
383 pub items: Vec<String>,
384 #[serde(default)]
385 pub ordered: bool,
386}
387
388#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
389pub struct KeyValue {
390 #[serde(skip_serializing_if = "Option::is_none")]
392 pub id: Option<String>,
393 pub pairs: Vec<KeyValuePair>,
394}
395
396#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
397pub struct KeyValuePair {
398 pub key: String,
399 pub value: String,
400}
401
402#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
403pub struct CodeBlock {
404 #[serde(skip_serializing_if = "Option::is_none")]
406 pub id: Option<String>,
407 pub code: String,
408 pub language: Option<String>,
409}
410
411#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
414pub struct Chart {
415 #[serde(skip_serializing_if = "Option::is_none")]
417 pub id: Option<String>,
418 #[serde(skip_serializing_if = "Option::is_none")]
419 pub title: Option<String>,
420 pub kind: ChartKind,
421 pub data: Vec<HashMap<String, serde_json::Value>>,
422 pub x_key: String,
423 pub y_keys: Vec<String>,
424 #[serde(skip_serializing_if = "Option::is_none")]
426 pub x_label: Option<String>,
427 #[serde(skip_serializing_if = "Option::is_none")]
429 pub y_label: Option<String>,
430 #[serde(default = "default_show_legend")]
432 pub show_legend: bool,
433 #[serde(skip_serializing_if = "Option::is_none")]
435 pub colors: Option<Vec<String>>,
436}
437
438fn default_show_legend() -> bool {
439 true
440}
441
442#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
443#[serde(rename_all = "snake_case")]
444pub enum ChartKind {
445 Bar,
446 Line,
447 Area,
448 Pie,
449}
450
451#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
454pub struct Scene3d {
455 #[serde(skip_serializing_if = "Option::is_none")]
456 pub id: Option<String>,
457 #[serde(skip_serializing_if = "Option::is_none")]
458 pub title: Option<String>,
459 #[serde(skip_serializing_if = "Option::is_none")]
460 pub description: Option<String>,
461 #[serde(default = "default_scene_height")]
462 pub height: u16,
463 #[serde(default)]
464 pub background: SceneBackground,
465 #[serde(default)]
466 pub camera: SceneCamera,
467 pub objects: Vec<SceneObject>,
468 #[serde(default)]
469 pub auto_rotate: bool,
470 #[serde(default = "default_true")]
471 pub controls: bool,
472 #[serde(default, skip_serializing_if = "Option::is_none")]
473 pub fallback: Option<String>,
474}
475
476fn default_scene_height() -> u16 {
477 440
478}
479
480#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
481#[serde(rename_all = "snake_case")]
482pub enum SceneBackground {
483 Transparent,
484 #[default]
485 Surface,
486 Muted,
487 Contrast,
488}
489
490#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
491pub struct SceneCamera {
492 #[serde(default = "default_camera_position")]
493 pub position: [f32; 3],
494 #[serde(default = "default_camera_target")]
495 pub target: [f32; 3],
496 #[serde(default = "default_camera_fov")]
497 pub fov: f32,
498}
499
500impl Default for SceneCamera {
501 fn default() -> Self {
502 Self {
503 position: default_camera_position(),
504 target: default_camera_target(),
505 fov: default_camera_fov(),
506 }
507 }
508}
509
510fn default_camera_position() -> [f32; 3] {
511 [4.5, 3.0, 6.0]
512}
513fn default_camera_target() -> [f32; 3] {
514 [0.0, 0.0, 0.0]
515}
516fn default_camera_fov() -> f32 {
517 42.0
518}
519
520#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
521#[serde(tag = "kind", rename_all = "snake_case")]
522pub enum SceneObject {
523 Primitive(ScenePrimitive),
524 Model(SceneModel),
525}
526
527#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
528pub struct ScenePrimitive {
529 pub id: String,
530 pub shape: ScenePrimitiveShape,
531 #[serde(default)]
532 pub position: [f32; 3],
533 #[serde(default)]
534 pub rotation: [f32; 3],
535 #[serde(default = "default_scene_scale")]
536 pub scale: [f32; 3],
537 #[serde(default)]
538 pub material: SceneMaterialRole,
539 #[serde(default, skip_serializing_if = "Option::is_none")]
540 pub label: Option<String>,
541 #[serde(default, skip_serializing_if = "Option::is_none")]
542 pub action_id: Option<String>,
543}
544
545fn default_scene_scale() -> [f32; 3] {
546 [1.0, 1.0, 1.0]
547}
548
549#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
550#[serde(rename_all = "snake_case")]
551pub enum ScenePrimitiveShape {
552 Box,
553 Sphere,
554 Cylinder,
555 Torus,
556 Plane,
557}
558
559#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
560#[serde(rename_all = "snake_case")]
561pub enum SceneMaterialRole {
562 #[default]
563 Primary,
564 Secondary,
565 Surface,
566 Muted,
567 Success,
568 Warning,
569 Info,
570}
571
572#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
573pub struct SceneModel {
574 pub id: String,
575 pub asset_id: String,
577 #[serde(default)]
578 pub position: [f32; 3],
579 #[serde(default)]
580 pub rotation: [f32; 3],
581 #[serde(default = "default_scene_scale")]
582 pub scale: [f32; 3],
583 #[serde(default, skip_serializing_if = "Option::is_none")]
584 pub label: Option<String>,
585 #[serde(default, skip_serializing_if = "Option::is_none")]
586 pub action_id: Option<String>,
587}
588
589#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
592pub struct Alert {
593 #[serde(skip_serializing_if = "Option::is_none")]
595 pub id: Option<String>,
596 pub title: String,
597 pub description: Option<String>,
598 #[serde(default)]
599 pub variant: AlertVariant,
600}
601
602#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
603#[serde(rename_all = "snake_case")]
604pub enum AlertVariant {
605 #[default]
606 Info,
607 Success,
608 Warning,
609 Error,
610}
611
612#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
613pub struct Progress {
614 #[serde(skip_serializing_if = "Option::is_none")]
616 pub id: Option<String>,
617 pub value: u8, pub label: Option<String>,
619}
620
621#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
622pub struct Toast {
623 #[serde(skip_serializing_if = "Option::is_none")]
624 pub id: Option<String>,
625 pub message: String,
626 #[serde(default)]
627 pub variant: AlertVariant,
628 #[serde(default = "default_toast_duration")]
630 pub duration: u32,
631 #[serde(default = "default_true")]
632 pub dismissible: bool,
633}
634
635fn default_toast_duration() -> u32 {
636 5000
637}
638
639#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
640pub struct Modal {
641 #[serde(skip_serializing_if = "Option::is_none")]
642 pub id: Option<String>,
643 pub title: String,
644 pub content: Vec<Component>,
645 pub footer: Option<Vec<Component>>,
646 #[serde(default)]
647 pub size: ModalSize,
648 #[serde(default = "default_true")]
649 pub closable: bool,
650}
651
652#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
653#[serde(rename_all = "snake_case")]
654pub enum ModalSize {
655 Small,
656 #[default]
657 Medium,
658 Large,
659 Full,
660}
661
662#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
663pub struct Spinner {
664 #[serde(skip_serializing_if = "Option::is_none")]
665 pub id: Option<String>,
666 #[serde(default)]
667 pub size: SpinnerSize,
668 pub label: Option<String>,
669}
670
671#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
672#[serde(rename_all = "snake_case")]
673pub enum SpinnerSize {
674 Small,
675 #[default]
676 Medium,
677 Large,
678}
679
680#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
681pub struct Skeleton {
682 #[serde(skip_serializing_if = "Option::is_none")]
683 pub id: Option<String>,
684 #[serde(default)]
685 pub variant: SkeletonVariant,
686 pub width: Option<String>,
687 pub height: Option<String>,
688}
689
690#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
691#[serde(rename_all = "snake_case")]
692pub enum SkeletonVariant {
693 #[default]
694 Text,
695 Circle,
696 Rectangle,
697}
698
699#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
700pub struct Textarea {
701 #[serde(skip_serializing_if = "Option::is_none")]
702 pub id: Option<String>,
703 pub name: String,
704 pub label: String,
705 pub placeholder: Option<String>,
706 #[serde(default = "default_textarea_rows")]
707 pub rows: u8,
708 #[serde(default)]
709 pub required: bool,
710 pub default_value: Option<String>,
711 #[serde(skip_serializing_if = "Option::is_none")]
712 pub error: Option<String>,
713}
714
715fn default_textarea_rows() -> u8 {
716 4
717}
718
719#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
723#[serde(rename_all = "snake_case")]
724pub enum Theme {
725 #[default]
726 Light,
727 Dark,
728 System,
729}
730
731#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
732pub struct UiResponse {
733 #[serde(default)]
735 pub id: Option<String>,
736 #[serde(default, skip_serializing_if = "Option::is_none")]
738 pub kit_id: Option<String>,
739 #[serde(default)]
741 pub theme: Theme,
742 pub components: Vec<Component>,
744}
745
746impl UiResponse {
747 pub fn new(components: Vec<Component>) -> Self {
748 Self {
749 id: None,
750 kit_id: None,
751 theme: Theme::default(),
752 components,
753 }
754 }
755
756 pub fn with_theme(mut self, theme: Theme) -> Self {
757 self.theme = theme;
758 self
759 }
760
761 pub fn with_id(mut self, id: impl Into<String>) -> Self {
762 self.id = Some(id.into());
763 self
764 }
765
766 pub fn with_kit_id(mut self, kit_id: impl Into<String>) -> Self {
767 self.kit_id = Some(kit_id.into());
768 self
769 }
770
771 pub fn to_content(self) -> crate::compat::Content {
772 let json = serde_json::to_vec(&self).unwrap_or_default();
773 crate::compat::Content {
774 role: "model".to_string(),
775 parts: vec![crate::compat::inline_data_part(MIME_TYPE_UI, json)],
776 }
777 }
778}
779
780#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
784#[serde(tag = "action", rename_all = "snake_case")]
785pub enum UiEvent {
786 FormSubmit {
788 action_id: String,
790 data: HashMap<String, serde_json::Value>,
792 },
793 ButtonClick {
795 action_id: String,
797 },
798 InputChange {
800 name: String,
802 value: serde_json::Value,
804 },
805 TabChange {
807 index: usize,
809 },
810}
811
812impl UiEvent {
813 pub fn to_user_message(&self) -> String {
815 match self {
816 UiEvent::FormSubmit { action_id, data } => {
817 let json = serde_json::to_string_pretty(data).unwrap_or_default();
818 format!(
819 "[UI Event: Form submitted]\nAction: {}\nData:\n{}",
820 action_id, json
821 )
822 }
823 UiEvent::ButtonClick { action_id } => {
824 format!("[UI Event: Button clicked]\nAction: {}", action_id)
825 }
826 UiEvent::InputChange { name, value } => {
827 format!(
828 "[UI Event: Input changed]\nField: {}\nValue: {}",
829 name, value
830 )
831 }
832 UiEvent::TabChange { index } => {
833 format!("[UI Event: Tab changed]\nIndex: {}", index)
834 }
835 }
836 }
837
838 pub fn to_content(&self) -> crate::compat::Content {
840 crate::compat::Content::new("user").with_text(self.to_user_message())
841 }
842}
843
844#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
848#[serde(rename_all = "snake_case")]
849pub enum UiOperation {
850 Replace,
852 Patch,
854 Append,
856 Remove,
858}
859
860#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
865pub struct UiUpdate {
866 pub target_id: String,
868 pub operation: UiOperation,
870 #[serde(skip_serializing_if = "Option::is_none")]
872 pub payload: Option<Component>,
873}
874
875impl UiUpdate {
876 pub fn replace(target_id: impl Into<String>, component: Component) -> Self {
878 Self {
879 target_id: target_id.into(),
880 operation: UiOperation::Replace,
881 payload: Some(component),
882 }
883 }
884
885 pub fn remove(target_id: impl Into<String>) -> Self {
887 Self {
888 target_id: target_id.into(),
889 operation: UiOperation::Remove,
890 payload: None,
891 }
892 }
893
894 pub fn append(target_id: impl Into<String>, component: Component) -> Self {
896 Self {
897 target_id: target_id.into(),
898 operation: UiOperation::Append,
899 payload: Some(component),
900 }
901 }
902
903 pub fn to_content(self) -> crate::compat::Content {
905 let json = serde_json::to_vec(&self).unwrap_or_default();
906 crate::compat::Content {
907 role: "model".to_string(),
908 parts: vec![crate::compat::inline_data_part(MIME_TYPE_UI_UPDATE, json)],
909 }
910 }
911}
912
913#[cfg(test)]
914mod tests {
915 use super::*;
916
917 #[test]
918 fn test_component_serialization_roundtrip() {
919 let text = Component::Text(Text {
920 id: Some("text-1".to_string()),
921 content: "Hello".to_string(),
922 variant: TextVariant::Body,
923 });
924
925 let json = serde_json::to_string(&text).unwrap();
926 let deserialized: Component = serde_json::from_str(&json).unwrap();
927
928 if let Component::Text(t) = deserialized {
929 assert_eq!(t.content, "Hello");
930 assert_eq!(t.id, Some("text-1".to_string()));
931 } else {
932 panic!("Expected Text component");
933 }
934 }
935
936 #[test]
937 fn test_ui_response_with_id() {
938 let ui = UiResponse::new(vec![])
939 .with_id("response-123")
940 .with_theme(Theme::Dark);
941
942 assert_eq!(ui.id, Some("response-123".to_string()));
943 assert!(matches!(ui.theme, Theme::Dark));
944 }
945
946 #[test]
947 fn test_badge_variants_serialize() {
948 let badge = Badge {
949 id: None,
950 label: "Test".to_string(),
951 variant: BadgeVariant::Success,
952 };
953 let json = serde_json::to_string(&badge).unwrap();
954 assert!(json.contains("success"));
955 }
956
957 #[test]
958 fn test_ui_event_to_message() {
959 let event = UiEvent::FormSubmit {
960 action_id: "submit".to_string(),
961 data: HashMap::new(),
962 };
963 let msg = event.to_user_message();
964 assert!(msg.contains("Form submitted"));
965 assert!(msg.contains("submit"));
966 }
967
968 #[test]
969 fn test_ui_update_replace() {
970 let update = UiUpdate::replace(
971 "target-1",
972 Component::Text(Text {
973 id: None,
974 content: "Updated".to_string(),
975 variant: TextVariant::Body,
976 }),
977 );
978
979 assert_eq!(update.target_id, "target-1");
980 assert!(matches!(update.operation, UiOperation::Replace));
981 assert!(update.payload.is_some());
982 }
983
984 #[test]
985 fn test_ui_update_remove() {
986 let update = UiUpdate::remove("to-delete");
987 assert_eq!(update.target_id, "to-delete");
988 assert!(matches!(update.operation, UiOperation::Remove));
989 assert!(update.payload.is_none());
990 }
991
992 #[test]
993 fn test_key_value_pairs() {
994 let kv = KeyValue {
995 id: Some("kv-1".to_string()),
996 pairs: vec![
997 KeyValuePair {
998 key: "Name".to_string(),
999 value: "Alice".to_string(),
1000 },
1001 KeyValuePair {
1002 key: "Age".to_string(),
1003 value: "30".to_string(),
1004 },
1005 ],
1006 };
1007
1008 let json = serde_json::to_string(&kv).unwrap();
1009 assert!(json.contains("pairs"));
1010 assert!(json.contains("Alice"));
1011 }
1012
1013 #[test]
1014 fn test_component_with_id_skips_none() {
1015 let text = Component::Text(Text {
1016 id: None,
1017 content: "No ID".to_string(),
1018 variant: TextVariant::Body,
1019 });
1020
1021 let json = serde_json::to_string(&text).unwrap();
1022 assert!(!json.contains("\"id\""));
1024 }
1025}