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,
98 #[serde(skip_serializing_if = "Option::is_none")]
99 pub icon: Option<String>,
100}
101
102impl CardSection {
103 pub fn new(heading: impl Into<String>, content: impl Into<String>) -> Self {
104 Self {
105 heading: heading.into(),
106 content: JsonValue::String(content.into()),
107 icon: None,
108 }
109 }
110
111 #[must_use]
112 pub fn value(heading: impl Into<String>, content: JsonValue) -> Self {
113 Self {
114 heading: heading.into(),
115 content,
116 icon: None,
117 }
118 }
119
120 #[must_use]
121 pub fn content_display(&self) -> String {
122 match &self.content {
123 JsonValue::String(s) => s.clone(),
124 JsonValue::Null => String::new(),
125 JsonValue::Array(items) => items
126 .iter()
127 .map(Self::scalar_text)
128 .collect::<Vec<_>>()
129 .join(", "),
130 other => Self::scalar_text(other),
131 }
132 }
133
134 fn scalar_text(value: &JsonValue) -> String {
135 match value {
136 JsonValue::String(s) => s.clone(),
137 JsonValue::Null => String::new(),
138 other => other.to_string(),
139 }
140 }
141
142 pub fn with_icon(mut self, icon: impl Into<String>) -> Self {
143 self.icon = Some(icon.into());
144 self
145 }
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
149pub struct CardCta {
150 pub id: String,
151 pub label: String,
152 pub message: String,
153 pub variant: CtaVariant,
154 #[serde(skip_serializing_if = "Option::is_none")]
155 pub icon: Option<String>,
156}
157
158impl CardCta {
159 pub fn new(
160 id: impl Into<String>,
161 label: impl Into<String>,
162 message: impl Into<String>,
163 variant: CtaVariant,
164 ) -> Self {
165 Self {
166 id: id.into(),
167 label: label.into(),
168 message: message.into(),
169 variant,
170 icon: None,
171 }
172 }
173
174 pub fn with_icon(mut self, icon: impl Into<String>) -> Self {
175 self.icon = Some(icon.into());
176 self
177 }
178}