use crate::compat::{Result, Tool, ToolContext};
use crate::schema::*;
use crate::tools::{LegacyProtocolOptions, render_ui_response_with_protocol};
use async_trait::async_trait;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::sync::Arc;
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct DashboardSection {
pub title: String,
#[serde(rename = "type")]
pub section_type: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub stats: Option<Vec<StatItem>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub severity: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub columns: Option<Vec<ColumnSpec>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rows: Option<Vec<HashMap<String, Value>>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub chart_type: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub data: Option<Vec<HashMap<String, Value>>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub x_key: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub y_keys: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pairs: Option<Vec<KeyValueItem>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub items: Option<Vec<String>>,
#[serde(default)]
pub ordered: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub code: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub language: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct StatItem {
pub label: String,
pub value: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ColumnSpec {
pub header: String,
pub key: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct KeyValueItem {
pub key: String,
pub value: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct RenderLayoutParams {
pub title: String,
#[serde(default)]
pub description: Option<String>,
pub sections: Vec<DashboardSection>,
#[serde(default)]
pub theme: Option<String>,
#[serde(flatten)]
pub protocol: LegacyProtocolOptions,
}
pub struct RenderLayoutTool;
impl RenderLayoutTool {
pub fn new() -> Self {
Self
}
}
impl Default for RenderLayoutTool {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl Tool for RenderLayoutTool {
fn name(&self) -> &str {
"render_layout"
}
fn description(&self) -> &str {
r#"Render a dashboard layout with multiple sections. Output example:
┌─────────────────────────────────────────────┐
│ System Status │
├─────────────────────────────────────────────┤
│ CPU: 45% ✓ │ Memory: 78% ⚠ │ Disk: 92% ✗ │
├─────────────────────────────────────────────┤
│ [Chart: Usage over time] │
├─────────────────────────────────────────────┤
│ Region: us-east-1 │ Version: 1.2.3 │
└─────────────────────────────────────────────┘
Section types: stats (label/value/status), table, chart, alert, text, key_value, list, code_block."#
}
fn parameters_schema(&self) -> Option<Value> {
Some(super::generate_gemini_schema::<RenderLayoutParams>())
}
async fn execute(&self, _ctx: Arc<dyn ToolContext>, args: Value) -> Result<Value> {
let params: RenderLayoutParams = serde_json::from_value(args.clone()).map_err(|e| {
crate::compat::AdkError::tool(format!("Invalid parameters: {}. Got: {}", e, args))
})?;
let protocol_options = params.protocol.clone();
let mut components = Vec::new();
components.push(Component::Text(Text {
id: None,
content: params.title,
variant: TextVariant::H2,
}));
if let Some(desc) = params.description {
components.push(Component::Text(Text {
id: None,
content: desc,
variant: TextVariant::Caption,
}));
}
for section in params.sections {
let section_component = build_section_component(section);
components.push(section_component);
}
let mut ui = UiResponse::new(components);
if let Some(theme_str) = params.theme {
let theme = match theme_str.to_lowercase().as_str() {
"dark" => Theme::Dark,
"system" => Theme::System,
_ => Theme::Light,
};
ui = ui.with_theme(theme);
}
render_ui_response_with_protocol(ui, &protocol_options, "layout")
}
}
fn build_section_component(section: DashboardSection) -> Component {
let mut card_content: Vec<Component> = Vec::new();
match section.section_type.as_str() {
"stats" => {
if let Some(stats) = section.stats {
for stat in stats {
let status_indicator = match stat.status.as_deref() {
Some("operational") | Some("ok") | Some("success") => "🟢 ",
Some("degraded") | Some("warning") => "🟡 ",
Some("down") | Some("error") | Some("outage") => "🔴 ",
_ => "",
};
card_content.push(Component::Text(Text {
id: None,
content: format!("{}{}: {}", status_indicator, stat.label, stat.value),
variant: TextVariant::Body,
}));
}
}
}
"text" => {
if let Some(text) = section.text {
card_content.push(Component::Text(Text {
id: None,
content: text,
variant: TextVariant::Body,
}));
}
}
"alert" => {
let variant = match section.severity.as_deref() {
Some("success") => AlertVariant::Success,
Some("warning") => AlertVariant::Warning,
Some("error") => AlertVariant::Error,
_ => AlertVariant::Info,
};
return Component::Alert(Alert {
id: None,
title: section.title,
description: section.message,
variant,
});
}
"table" => {
if let (Some(cols), Some(rows)) = (section.columns, section.rows) {
let table_columns: Vec<TableColumn> = cols
.into_iter()
.map(|c| TableColumn {
header: c.header,
accessor_key: c.key,
sortable: true,
})
.collect();
card_content.push(Component::Table(Table {
id: None,
columns: table_columns,
data: rows,
sortable: false,
page_size: None,
striped: false,
}));
}
}
"chart" => {
if let (Some(data), Some(x), Some(y)) = (section.data, section.x_key, section.y_keys) {
let kind = match section.chart_type.as_deref() {
Some("line") => ChartKind::Line,
Some("area") => ChartKind::Area,
Some("pie") => ChartKind::Pie,
_ => ChartKind::Bar,
};
card_content.push(Component::Chart(Chart {
id: None,
title: None,
kind,
data,
x_key: x,
y_keys: y,
x_label: None,
y_label: None,
show_legend: true,
colors: None,
}));
}
}
"key_value" => {
if let Some(pairs) = section.pairs {
let kv_pairs: Vec<KeyValuePair> = pairs
.into_iter()
.map(|p| KeyValuePair {
key: p.key,
value: p.value,
})
.collect();
card_content.push(Component::KeyValue(KeyValue {
id: None,
pairs: kv_pairs,
}));
}
}
"list" => {
if let Some(items) = section.items {
card_content.push(Component::List(List {
id: None,
items,
ordered: section.ordered,
}));
}
}
"code_block" => {
if let Some(code) = section.code {
card_content.push(Component::CodeBlock(CodeBlock {
id: None,
code,
language: section.language,
}));
}
}
_ => {
card_content.push(Component::Text(Text {
id: None,
content: format!("Unknown section type: {}", section.section_type),
variant: TextVariant::Caption,
}));
}
}
if card_content.is_empty() {
card_content.push(Component::Text(Text {
id: None,
content: "(No content)".to_string(),
variant: TextVariant::Caption,
}));
}
Component::Card(Card {
id: None,
title: Some(section.title),
description: None,
content: card_content,
footer: None,
})
}