use super::form_field::FormField;
use super::html::{
HtmlBuilder, base_styles, html_escape, json_to_js_literal, mcp_app_bridge_script,
};
use crate::error::McpDomainResult;
use crate::services::ui_renderer::{CspPolicy, UiRenderer, UiResource};
use async_trait::async_trait;
use serde_json::Value as JsonValue;
use systemprompt_models::a2a::Artifact;
use systemprompt_models::artifacts::ArtifactType;
#[derive(Debug, Clone, Copy, Default)]
pub struct FormRenderer;
impl FormRenderer {
pub const fn new() -> Self {
Self
}
fn extract_fields(artifact: &Artifact) -> Vec<FormField> {
let mut fields = Vec::new();
if let Some(hints) = &artifact.metadata.rendering_hints {
if let Some(field_defs) = hints.get("fields").and_then(JsonValue::as_array) {
for def in field_defs {
if let Some(field) = FormField::from_json(def) {
fields.push(field);
}
}
}
}
for part in &artifact.parts {
if let Some(data) = part.as_data() {
if let Some(form_fields) = data.get("fields").and_then(JsonValue::as_array) {
for def in form_fields {
if let Some(field) = FormField::from_json(def) {
if !fields.iter().any(|f| f.name == field.name) {
fields.push(field);
}
}
}
}
}
}
fields
}
fn extract_submit_tool(artifact: &Artifact) -> Option<String> {
artifact
.metadata
.rendering_hints
.as_ref()
.and_then(|h| h.get("submit_tool"))
.and_then(JsonValue::as_str)
.map(String::from)
}
}
#[async_trait]
impl UiRenderer for FormRenderer {
fn artifact_type(&self) -> ArtifactType {
ArtifactType::Form
}
async fn render(&self, artifact: &Artifact) -> McpDomainResult<UiResource> {
let fields = Self::extract_fields(artifact);
let submit_tool = Self::extract_submit_tool(artifact);
let title = artifact.title.as_deref().unwrap_or("Form");
let fields_html: String = fields.iter().map(FormField::render_html).collect();
let fields_json: Vec<JsonValue> = fields
.iter()
.map(|f| {
serde_json::json!({
"name": f.name,
"type": f.field_type,
"required": f.required
})
})
.collect();
let body = format!(
r#"<div class="container">
{title_html}
{description_html}
<form id="mcp-form" class="mcp-form">
{fields}
<div class="form-actions">
<button type="submit" class="submit-btn">Submit</button>
<button type="reset" class="reset-btn">Reset</button>
</div>
</form>
<div id="form-message" class="form-message" style="display: none;"></div>
</div>"#,
title_html = if title.is_empty() {
String::new()
} else {
format!(r#"<h1 class="mcp-app-title">{}</h1>"#, html_escape(title))
},
description_html = artifact
.description
.as_ref()
.map_or_else(String::new, |d| format!(
r#"<p class="mcp-app-description">{}</p>"#,
html_escape(d)
)),
fields = fields_html,
);
let script = format!(
"{bridge}\nwindow.FORM_FIELDS = {fields_json};\nwindow.FORM_SUBMIT_TOOL = \
{submit_tool};\n{app}",
bridge = mcp_app_bridge_script(),
fields_json = json_to_js_literal(&serde_json::json!(fields_json)),
submit_tool = submit_tool.map_or_else(|| "null".to_owned(), |t| format!("\"{t}\"")),
app = include_str!("assets/js/form.js"),
);
let html = HtmlBuilder::new(title)
.add_style(base_styles())
.add_style(form_styles())
.body(&body)
.add_script(&script)
.build();
Ok(UiResource::new(html).with_csp(self.csp_policy()))
}
fn csp_policy(&self) -> CspPolicy {
CspPolicy::strict()
}
}
const fn form_styles() -> &'static str {
include_str!("assets/css/form.css")
}