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