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