systemprompt_models/artifacts/card/
mod.rs1mod artifact;
12
13use schemars::JsonSchema;
14use serde::{Deserialize, Serialize};
15use serde_json::Value as JsonValue;
16use systemprompt_identifiers::SkillId;
17
18pub use artifact::PresentationCardArtifact;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
27#[serde(rename_all = "lowercase")]
28pub enum CardTheme {
29 #[default]
30 Gradient,
31 Plain,
32 Muted,
33 #[serde(other)]
34 Unknown,
35}
36
37impl CardTheme {
38 #[must_use]
39 pub const fn class_suffix(self) -> &'static str {
40 match self {
41 Self::Gradient | Self::Unknown => "gradient",
42 Self::Plain => "plain",
43 Self::Muted => "muted",
44 }
45 }
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
54#[serde(rename_all = "lowercase")]
55pub enum CtaVariant {
56 #[default]
57 Primary,
58 Secondary,
59 Danger,
60 #[serde(other)]
61 Unknown,
62}
63
64impl CtaVariant {
65 #[must_use]
66 pub const fn class_suffix(self) -> &'static str {
67 match self {
68 Self::Primary | Self::Unknown => "primary",
69 Self::Secondary => "secondary",
70 Self::Danger => "danger",
71 }
72 }
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
76pub struct PresentationCardResponse {
77 #[serde(rename = "x-artifact-type")]
78 pub artifact_type: String,
79 pub title: String,
80 #[serde(skip_serializing_if = "Option::is_none")]
81 pub subtitle: Option<String>,
82 pub sections: Vec<CardSection>,
83 #[serde(skip_serializing_if = "Vec::is_empty", default)]
84 pub ctas: Vec<CardCta>,
85 pub theme: CardTheme,
86 #[serde(skip_serializing_if = "Option::is_none")]
87 pub execution_id: Option<String>,
88 #[serde(skip_serializing_if = "Option::is_none")]
89 pub skill_id: Option<SkillId>,
90 #[serde(skip_serializing_if = "Option::is_none")]
91 pub skill_name: Option<String>,
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
95pub struct CardSection {
96 pub heading: String,
97 pub content: JsonValue,
99 #[serde(skip_serializing_if = "Option::is_none")]
100 pub icon: Option<String>,
101}
102
103impl CardSection {
104 pub fn new(heading: impl Into<String>, content: impl Into<String>) -> Self {
105 Self {
106 heading: heading.into(),
107 content: JsonValue::String(content.into()),
108 icon: None,
109 }
110 }
111
112 #[must_use]
113 pub fn value(heading: impl Into<String>, content: JsonValue) -> Self {
115 Self {
116 heading: heading.into(),
117 content,
118 icon: None,
119 }
120 }
121
122 #[must_use]
123 pub fn content_display(&self) -> String {
124 match &self.content {
125 JsonValue::String(s) => s.clone(),
126 JsonValue::Null => String::new(),
127 JsonValue::Array(items) => items
128 .iter()
129 .map(Self::scalar_text)
130 .collect::<Vec<_>>()
131 .join(", "),
132 other => Self::scalar_text(other),
133 }
134 }
135
136 fn scalar_text(value: &JsonValue) -> String {
138 match value {
139 JsonValue::String(s) => s.clone(),
140 JsonValue::Null => String::new(),
141 other => other.to_string(),
142 }
143 }
144
145 pub fn with_icon(mut self, icon: impl Into<String>) -> Self {
146 self.icon = Some(icon.into());
147 self
148 }
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
152pub struct CardCta {
153 pub id: String,
154 pub label: String,
155 pub message: String,
156 pub variant: CtaVariant,
157 #[serde(skip_serializing_if = "Option::is_none")]
158 pub icon: Option<String>,
159}
160
161impl CardCta {
162 pub fn new(
163 id: impl Into<String>,
164 label: impl Into<String>,
165 message: impl Into<String>,
166 variant: CtaVariant,
167 ) -> Self {
168 Self {
169 id: id.into(),
170 label: label.into(),
171 message: message.into(),
172 variant,
173 icon: None,
174 }
175 }
176
177 pub fn with_icon(mut self, icon: impl Into<String>) -> Self {
178 self.icon = Some(icon.into());
179 self
180 }
181}