1use 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::collections::HashMap;
9use std::sync::Arc;
10
11#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
23pub struct DashboardSection {
24 pub title: String,
26 #[serde(rename = "type")]
28 pub section_type: String,
29 #[serde(default, skip_serializing_if = "Option::is_none")]
31 pub stats: Option<Vec<StatItem>>,
32 #[serde(default, skip_serializing_if = "Option::is_none")]
34 pub text: Option<String>,
35 #[serde(default, skip_serializing_if = "Option::is_none")]
37 pub message: Option<String>,
38 #[serde(default, skip_serializing_if = "Option::is_none")]
40 pub severity: Option<String>,
41 #[serde(default, skip_serializing_if = "Option::is_none")]
43 pub columns: Option<Vec<ColumnSpec>>,
44 #[serde(default, skip_serializing_if = "Option::is_none")]
46 pub rows: Option<Vec<HashMap<String, Value>>>,
47 #[serde(default, skip_serializing_if = "Option::is_none")]
49 pub chart_type: Option<String>,
50 #[serde(default, skip_serializing_if = "Option::is_none")]
52 pub data: Option<Vec<HashMap<String, Value>>>,
53 #[serde(default, skip_serializing_if = "Option::is_none")]
55 pub x_key: Option<String>,
56 #[serde(default, skip_serializing_if = "Option::is_none")]
58 pub y_keys: Option<Vec<String>>,
59 #[serde(default, skip_serializing_if = "Option::is_none")]
61 pub pairs: Option<Vec<KeyValueItem>>,
62 #[serde(default, skip_serializing_if = "Option::is_none")]
64 pub items: Option<Vec<String>>,
65 #[serde(default)]
67 pub ordered: bool,
68 #[serde(default, skip_serializing_if = "Option::is_none")]
70 pub code: Option<String>,
71 #[serde(default, skip_serializing_if = "Option::is_none")]
73 pub language: Option<String>,
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
77pub struct StatItem {
78 pub label: String,
80 pub value: String,
82 #[serde(default, skip_serializing_if = "Option::is_none")]
84 pub status: Option<String>,
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
88pub struct ColumnSpec {
89 pub header: String,
91 pub key: String,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
97pub struct KeyValueItem {
98 pub key: String,
100 pub value: String,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
106pub struct RenderLayoutParams {
107 pub title: String,
109 #[serde(default)]
111 pub description: Option<String>,
112 pub sections: Vec<DashboardSection>,
114 #[serde(default)]
116 pub theme: Option<String>,
117 #[serde(flatten)]
119 pub protocol: LegacyProtocolOptions,
120}
121
122pub struct RenderLayoutTool;
165
166impl RenderLayoutTool {
167 pub fn new() -> Self {
168 Self
169 }
170}
171
172impl Default for RenderLayoutTool {
173 fn default() -> Self {
174 Self::new()
175 }
176}
177
178#[async_trait]
179impl Tool for RenderLayoutTool {
180 fn name(&self) -> &str {
181 "render_layout"
182 }
183
184 fn description(&self) -> &str {
185 r#"Render a dashboard layout with multiple sections. Output example:
186┌─────────────────────────────────────────────┐
187│ System Status │
188├─────────────────────────────────────────────┤
189│ CPU: 45% ✓ │ Memory: 78% ⚠ │ Disk: 92% ✗ │
190├─────────────────────────────────────────────┤
191│ [Chart: Usage over time] │
192├─────────────────────────────────────────────┤
193│ Region: us-east-1 │ Version: 1.2.3 │
194└─────────────────────────────────────────────┘
195Section types: stats (label/value/status), table, chart, alert, text, key_value, list, code_block."#
196 }
197
198 fn parameters_schema(&self) -> Option<Value> {
199 Some(super::generate_gemini_schema::<RenderLayoutParams>())
200 }
201
202 async fn execute(&self, _ctx: Arc<dyn ToolContext>, args: Value) -> Result<Value> {
203 let params: RenderLayoutParams = serde_json::from_value(args.clone()).map_err(|e| {
204 adk_core::AdkError::Tool(format!("Invalid parameters: {}. Got: {}", e, args))
205 })?;
206 let protocol_options = params.protocol.clone();
207
208 let mut components = Vec::new();
209
210 components.push(Component::Text(Text {
212 id: None,
213 content: params.title,
214 variant: TextVariant::H2,
215 }));
216
217 if let Some(desc) = params.description {
219 components.push(Component::Text(Text {
220 id: None,
221 content: desc,
222 variant: TextVariant::Caption,
223 }));
224 }
225
226 for section in params.sections {
228 let section_component = build_section_component(section);
229 components.push(section_component);
230 }
231
232 let mut ui = UiResponse::new(components);
233
234 if let Some(theme_str) = params.theme {
236 let theme = match theme_str.to_lowercase().as_str() {
237 "dark" => Theme::Dark,
238 "system" => Theme::System,
239 _ => Theme::Light,
240 };
241 ui = ui.with_theme(theme);
242 }
243
244 render_ui_response_with_protocol(ui, &protocol_options, "layout")
245 }
246}
247
248fn build_section_component(section: DashboardSection) -> Component {
249 let mut card_content: Vec<Component> = Vec::new();
250
251 match section.section_type.as_str() {
252 "stats" => {
253 if let Some(stats) = section.stats {
254 for stat in stats {
256 let status_indicator = match stat.status.as_deref() {
257 Some("operational") | Some("ok") | Some("success") => "🟢 ",
258 Some("degraded") | Some("warning") => "🟡 ",
259 Some("down") | Some("error") | Some("outage") => "🔴 ",
260 _ => "",
261 };
262 card_content.push(Component::Text(Text {
263 id: None,
264 content: format!("{}{}: {}", status_indicator, stat.label, stat.value),
265 variant: TextVariant::Body,
266 }));
267 }
268 }
269 }
270 "text" => {
271 if let Some(text) = section.text {
272 card_content.push(Component::Text(Text {
273 id: None,
274 content: text,
275 variant: TextVariant::Body,
276 }));
277 }
278 }
279 "alert" => {
280 let variant = match section.severity.as_deref() {
281 Some("success") => AlertVariant::Success,
282 Some("warning") => AlertVariant::Warning,
283 Some("error") => AlertVariant::Error,
284 _ => AlertVariant::Info,
285 };
286 return Component::Alert(Alert {
287 id: None,
288 title: section.title,
289 description: section.message,
290 variant,
291 });
292 }
293 "table" => {
294 if let (Some(cols), Some(rows)) = (section.columns, section.rows) {
295 let table_columns: Vec<TableColumn> = cols
296 .into_iter()
297 .map(|c| TableColumn { header: c.header, accessor_key: c.key, sortable: true })
298 .collect();
299 card_content.push(Component::Table(Table {
300 id: None,
301 columns: table_columns,
302 data: rows,
303 sortable: false,
304 page_size: None,
305 striped: false,
306 }));
307 }
308 }
309 "chart" => {
310 if let (Some(data), Some(x), Some(y)) = (section.data, section.x_key, section.y_keys) {
311 let kind = match section.chart_type.as_deref() {
312 Some("line") => ChartKind::Line,
313 Some("area") => ChartKind::Area,
314 Some("pie") => ChartKind::Pie,
315 _ => ChartKind::Bar,
316 };
317 card_content.push(Component::Chart(Chart {
318 id: None,
319 title: None,
320 kind,
321 data,
322 x_key: x,
323 y_keys: y,
324 x_label: None,
325 y_label: None,
326 show_legend: true,
327 colors: None,
328 }));
329 }
330 }
331 "key_value" => {
332 if let Some(pairs) = section.pairs {
333 let kv_pairs: Vec<KeyValuePair> = pairs
334 .into_iter()
335 .map(|p| KeyValuePair { key: p.key, value: p.value })
336 .collect();
337 card_content.push(Component::KeyValue(KeyValue { id: None, pairs: kv_pairs }));
338 }
339 }
340 "list" => {
341 if let Some(items) = section.items {
342 card_content.push(Component::List(List {
343 id: None,
344 items,
345 ordered: section.ordered,
346 }));
347 }
348 }
349 "code_block" => {
350 if let Some(code) = section.code {
351 card_content.push(Component::CodeBlock(CodeBlock {
352 id: None,
353 code,
354 language: section.language,
355 }));
356 }
357 }
358 _ => {
359 card_content.push(Component::Text(Text {
361 id: None,
362 content: format!("Unknown section type: {}", section.section_type),
363 variant: TextVariant::Caption,
364 }));
365 }
366 }
367
368 if card_content.is_empty() {
370 card_content.push(Component::Text(Text {
371 id: None,
372 content: "(No content)".to_string(),
373 variant: TextVariant::Caption,
374 }));
375 }
376
377 Component::Card(Card {
378 id: None,
379 title: Some(section.title),
380 description: None,
381 content: card_content,
382 footer: None,
383 })
384}