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