Skip to main content

systemprompt_models/artifacts/card/
mod.rs

1use crate::artifacts::metadata::ExecutionMetadata;
2use crate::artifacts::traits::Artifact;
3use crate::artifacts::types::ArtifactType;
4use crate::execution::context::RequestContext;
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7use serde_json::{Value as JsonValue, json};
8use systemprompt_identifiers::SkillId;
9
10#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
11pub struct PresentationCardResponse {
12    #[serde(rename = "x-artifact-type")]
13    pub artifact_type: String,
14    pub title: String,
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub subtitle: Option<String>,
17    pub sections: Vec<CardSection>,
18    #[serde(skip_serializing_if = "Vec::is_empty", default)]
19    pub ctas: Vec<CardCta>,
20    pub theme: String,
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub execution_id: Option<String>,
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub skill_id: Option<String>,
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub skill_name: Option<String>,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
30pub struct CardSection {
31    pub heading: String,
32    pub content: String,
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub icon: Option<String>,
35}
36
37impl CardSection {
38    pub fn new(heading: impl Into<String>, content: impl Into<String>) -> Self {
39        Self {
40            heading: heading.into(),
41            content: content.into(),
42            icon: None,
43        }
44    }
45
46    pub fn with_icon(mut self, icon: impl Into<String>) -> Self {
47        self.icon = Some(icon.into());
48        self
49    }
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
53pub struct CardCta {
54    pub id: String,
55    pub label: String,
56    pub message: String,
57    pub variant: String,
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub icon: Option<String>,
60}
61
62impl CardCta {
63    pub fn new(
64        id: impl Into<String>,
65        label: impl Into<String>,
66        message: impl Into<String>,
67        variant: impl Into<String>,
68    ) -> Self {
69        Self {
70            id: id.into(),
71            label: label.into(),
72            message: message.into(),
73            variant: variant.into(),
74            icon: None,
75        }
76    }
77
78    pub fn with_icon(mut self, icon: impl Into<String>) -> Self {
79        self.icon = Some(icon.into());
80        self
81    }
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
85pub struct PresentationCardArtifact {
86    #[serde(rename = "x-artifact-type")]
87    #[serde(default = "default_card_artifact_type")]
88    pub artifact_type: String,
89    pub title: String,
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub subtitle: Option<String>,
92    pub sections: Vec<CardSection>,
93    #[serde(default, skip_serializing_if = "Vec::is_empty")]
94    pub ctas: Vec<CardCta>,
95    #[serde(default = "default_theme")]
96    pub theme: String,
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub execution_id: Option<String>,
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub skill_id: Option<String>,
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub skill_name: Option<String>,
103    #[serde(skip)]
104    #[schemars(skip)]
105    metadata: ExecutionMetadata,
106}
107
108fn default_theme() -> String {
109    "gradient".to_string()
110}
111
112fn default_card_artifact_type() -> String {
113    "presentation_card".to_string()
114}
115
116impl PresentationCardArtifact {
117    pub const ARTIFACT_TYPE_STR: &'static str = "presentation_card";
118
119    pub fn new(title: impl Into<String>, ctx: &RequestContext) -> Self {
120        Self {
121            artifact_type: "presentation_card".to_string(),
122            title: title.into(),
123            subtitle: None,
124            sections: Vec::new(),
125            ctas: Vec::new(),
126            theme: default_theme(),
127            execution_id: None,
128            skill_id: None,
129            skill_name: None,
130            metadata: ExecutionMetadata::with_request(ctx),
131        }
132    }
133
134    pub fn with_subtitle(mut self, subtitle: impl Into<String>) -> Self {
135        self.subtitle = Some(subtitle.into());
136        self
137    }
138
139    pub fn with_sections(mut self, sections: Vec<CardSection>) -> Self {
140        self.sections = sections;
141        self
142    }
143
144    pub fn add_section(mut self, section: CardSection) -> Self {
145        self.sections.push(section);
146        self
147    }
148
149    pub fn with_ctas(mut self, ctas: Vec<CardCta>) -> Self {
150        self.ctas = ctas;
151        self
152    }
153
154    pub fn add_cta(mut self, cta: CardCta) -> Self {
155        self.ctas.push(cta);
156        self
157    }
158
159    pub fn with_theme(mut self, theme: impl Into<String>) -> Self {
160        self.theme = theme.into();
161        self
162    }
163
164    pub fn with_execution_id(mut self, id: impl Into<String>) -> Self {
165        let id_str = id.into();
166        self.execution_id = Some(id_str.clone());
167        self.metadata.execution_id = Some(id_str);
168        self
169    }
170
171    pub fn with_skill(
172        mut self,
173        skill_id: impl Into<SkillId>,
174        skill_name: impl Into<String>,
175    ) -> Self {
176        let id = skill_id.into();
177        self.skill_id = Some(id.to_string());
178        self.skill_name = Some(skill_name.into());
179        self.metadata.skill_id = Some(id);
180        self
181    }
182}
183
184impl Artifact for PresentationCardArtifact {
185    fn artifact_type(&self) -> ArtifactType {
186        ArtifactType::PresentationCard
187    }
188
189    fn to_schema(&self) -> JsonValue {
190        json!({
191            "type": "object",
192            "properties": {
193                "title": {
194                    "type": "string",
195                    "description": "Card title"
196                },
197                "subtitle": {
198                    "type": "string",
199                    "description": "Card subtitle"
200                },
201                "sections": {
202                    "type": "array",
203                    "description": "Content sections",
204                    "items": {
205                        "type": "object",
206                        "properties": {
207                            "heading": {"type": "string"},
208                            "content": {"type": "string"},
209                            "icon": {"type": "string"}
210                        },
211                        "required": ["heading", "content"]
212                    }
213                },
214                "ctas": {
215                    "type": "array",
216                    "description": "Call-to-action buttons",
217                    "items": {
218                        "type": "object",
219                        "properties": {
220                            "id": {"type": "string"},
221                            "label": {"type": "string"},
222                            "message": {"type": "string"},
223                            "variant": {"type": "string"},
224                            "icon": {"type": "string"}
225                        },
226                        "required": ["id", "label", "message", "variant"]
227                    }
228                },
229                "theme": {
230                    "type": "string",
231                    "description": "Card theme",
232                    "default": "gradient"
233                },
234                "_execution_id": {
235                    "type": "string",
236                    "description": "Execution ID for tracking"
237                }
238            },
239            "required": ["title", "sections"],
240            "x-artifact-type": "presentation_card",
241            "x-presentation-hints": {
242                "theme": self.theme
243            }
244        })
245    }
246}