adk_ui/tools/
render_card.rs1use crate::schema::*;
2use crate::tools::{LegacyProtocolOptions, render_ui_response_with_protocol};
3use adk_core::{Result, Tool, ToolContext};
4use async_trait::async_trait;
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8use std::sync::Arc;
9
10#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
12pub struct RenderCardParams {
13 pub title: String,
15 #[serde(default)]
17 pub description: Option<String>,
18 pub content: String,
20 #[serde(default)]
22 pub actions: Vec<CardAction>,
23 #[serde(flatten)]
25 pub protocol: LegacyProtocolOptions,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
29pub struct CardAction {
30 pub label: String,
32 pub action_id: String,
34 #[serde(default = "default_variant")]
36 pub variant: String,
37}
38
39fn default_variant() -> String {
40 "primary".to_string()
41}
42
43pub struct RenderCardTool;
61
62impl RenderCardTool {
63 pub fn new() -> Self {
64 Self
65 }
66}
67
68impl Default for RenderCardTool {
69 fn default() -> Self {
70 Self::new()
71 }
72}
73
74#[async_trait]
75impl Tool for RenderCardTool {
76 fn name(&self) -> &str {
77 "render_card"
78 }
79
80 fn description(&self) -> &str {
81 r#"Render an information card. Output example:
82┌─────────────────────────────┐
83│ Welcome │
84│ Your account is ready │
85│ ─────────────────────────── │
86│ Click below to get started. │
87│ [Get Started] │
88└─────────────────────────────┘
89Use for status updates, summaries, or any structured info with optional action buttons."#
90 }
91
92 fn parameters_schema(&self) -> Option<Value> {
93 Some(super::generate_gemini_schema::<RenderCardParams>())
94 }
95
96 async fn execute(&self, _ctx: Arc<dyn ToolContext>, args: Value) -> Result<Value> {
97 let params: RenderCardParams = serde_json::from_value(args)
98 .map_err(|e| adk_core::AdkError::Tool(format!("Invalid parameters: {}", e)))?;
99 let protocol_options = params.protocol.clone();
100
101 let content = vec![Component::Text(Text {
103 id: None,
104 content: params.content,
105 variant: TextVariant::Body,
106 })];
107
108 let footer = if params.actions.is_empty() {
110 None
111 } else {
112 Some(
113 params
114 .actions
115 .into_iter()
116 .map(|action| {
117 let variant = match action.variant.as_str() {
118 "secondary" => ButtonVariant::Secondary,
119 "danger" => ButtonVariant::Danger,
120 "ghost" => ButtonVariant::Ghost,
121 "outline" => ButtonVariant::Outline,
122 _ => ButtonVariant::Primary,
123 };
124 Component::Button(Button {
125 id: None,
126 label: action.label,
127 action_id: action.action_id,
128 variant,
129 disabled: false,
130 icon: None,
131 })
132 })
133 .collect(),
134 )
135 };
136
137 let ui = UiResponse::new(vec![Component::Card(Card {
138 id: None,
139 title: Some(params.title),
140 description: params.description,
141 content,
142 footer,
143 })]);
144
145 render_ui_response_with_protocol(ui, &protocol_options, "card")
146 }
147}