adk_ui/tools/
render_form.rs

1use crate::schema::*;
2use adk_core::{Result, Tool, ToolContext};
3use async_trait::async_trait;
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7use std::sync::Arc;
8
9/// Parameters for the render_form tool
10#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
11pub struct RenderFormParams {
12    /// Title of the form
13    pub title: String,
14    /// Optional description
15    #[serde(default)]
16    pub description: Option<String>,
17    /// Form fields to render
18    pub fields: Vec<FormField>,
19    /// Action ID for form submission
20    #[serde(default = "default_submit_action")]
21    pub submit_action: String,
22    /// Submit button label
23    #[serde(default = "default_submit_label")]
24    pub submit_label: String,
25    /// Theme: "light", "dark", or "system" (default: "light")
26    #[serde(default)]
27    pub theme: Option<String>,
28}
29
30fn default_submit_action() -> String {
31    "form_submit".to_string()
32}
33
34fn default_submit_label() -> String {
35    "Submit".to_string()
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
39pub struct FormField {
40    /// Field name (used as key in submission)
41    pub name: String,
42    /// Label displayed to user
43    pub label: String,
44    /// Field type: text, email, password, number, date, select
45    #[serde(rename = "type", default = "default_field_type")]
46    pub field_type: String,
47    /// Placeholder text
48    #[serde(default)]
49    pub placeholder: Option<String>,
50    /// Whether the field is required
51    #[serde(default)]
52    pub required: bool,
53    /// Options for select fields
54    #[serde(default)]
55    pub options: Vec<SelectOption>,
56}
57
58fn default_field_type() -> String {
59    "text".to_string()
60}
61
62/// Tool for rendering forms to collect user input.
63///
64/// This tool generates form UI components that allow agents to collect
65/// structured input from users. The form includes various field types
66/// and returns submitted data via `UiEvent::FormSubmit`.
67///
68/// # Supported Field Types
69///
70/// - `text`: Single-line text input (default)
71/// - `email`: Email address input with validation
72/// - `password`: Password input (masked)
73/// - `number`: Numeric input
74/// - `select`: Dropdown selection from options
75/// - `textarea`: Multi-line text input
76///
77/// # Example JSON Parameters
78///
79/// ```json
80/// {
81///   "title": "Contact Form",
82///   "description": "Please fill out your details",
83///   "fields": [
84///     { "name": "email", "label": "Email", "type": "email", "required": true },
85///     { "name": "message", "label": "Message", "type": "textarea" }
86///   ],
87///   "submit_label": "Send"
88/// }
89/// ```
90pub struct RenderFormTool;
91
92impl RenderFormTool {
93    pub fn new() -> Self {
94        Self
95    }
96}
97
98impl Default for RenderFormTool {
99    fn default() -> Self {
100        Self::new()
101    }
102}
103
104#[async_trait]
105impl Tool for RenderFormTool {
106    fn name(&self) -> &str {
107        "render_form"
108    }
109
110    fn description(&self) -> &str {
111        r#"Render a form to collect user input. Output example:
112┌─────────────────────────┐
113│ Registration Form       │
114│ ─────────────────────── │
115│ Name*: [___________]    │
116│ Email*: [___________]   │
117│ Password*: [___________]│
118│         [Register]      │
119└─────────────────────────┘
120Use field types: text, email, password, number, select, textarea. Set required=true for mandatory fields."#
121    }
122
123    fn parameters_schema(&self) -> Option<Value> {
124        Some(super::generate_gemini_schema::<RenderFormParams>())
125    }
126
127    async fn execute(&self, _ctx: Arc<dyn ToolContext>, args: Value) -> Result<Value> {
128        let params: RenderFormParams = serde_json::from_value(args)
129            .map_err(|e| adk_core::AdkError::Tool(format!("Invalid parameters: {}", e)))?;
130
131        // Build the form UI
132        let mut form_content: Vec<Component> = Vec::new();
133
134        for field in params.fields {
135            let component = match field.field_type.as_str() {
136                "number" => Component::NumberInput(NumberInput {
137                    id: None,
138                    name: field.name,
139                    label: field.label,
140                    min: None,
141                    max: None,
142                    step: None,
143                    required: field.required,
144                    default_value: None,
145                    error: None,
146                }),
147                "select" => Component::Select(Select {
148                    id: None,
149                    name: field.name,
150                    label: field.label,
151                    options: field.options,
152                    required: field.required,
153                    error: None,
154                }),
155                "textarea" => Component::Textarea(Textarea {
156                    id: None,
157                    name: field.name,
158                    label: field.label,
159                    placeholder: field.placeholder,
160                    rows: 4,
161                    required: field.required,
162                    default_value: None,
163                    error: None,
164                }),
165                _ => Component::TextInput(TextInput {
166                    id: None,
167                    name: field.name,
168                    label: field.label,
169                    input_type: field.field_type.clone(),
170                    placeholder: field.placeholder,
171                    required: field.required,
172                    default_value: None,
173                    min_length: None,
174                    max_length: None,
175                    error: None,
176                }),
177            };
178            form_content.push(component);
179        }
180
181        // Add submit button
182        form_content.push(Component::Button(Button {
183            id: None,
184            label: params.submit_label,
185            action_id: params.submit_action,
186            variant: ButtonVariant::Primary,
187            disabled: false,
188            icon: None,
189        }));
190
191        // Wrap in a card
192        let mut ui = UiResponse::new(vec![Component::Card(Card {
193            id: None,
194            title: Some(params.title),
195            description: params.description,
196            content: form_content,
197            footer: None,
198        })]);
199
200        // Apply theme if specified
201        if let Some(theme_str) = params.theme {
202            let theme = match theme_str.to_lowercase().as_str() {
203                "dark" => Theme::Dark,
204                "system" => Theme::System,
205                _ => Theme::Light,
206            };
207            ui = ui.with_theme(theme);
208        }
209
210        // Return as JSON - the framework will convert to Part::InlineData
211        serde_json::to_value(ui)
212            .map_err(|e| adk_core::AdkError::Tool(format!("Failed to serialize UI: {}", e)))
213    }
214}