1use gpui_kit::assets::IconName;
4
5use crate::icon::ChatIcon;
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
9pub enum ComponentKind {
10 Chat,
12 Embedder,
14 Store,
16 Parser,
18 Chunker,
20 Retriever,
22 Memory,
24 Vision,
26 References,
28 Other,
30}
31
32impl ComponentKind {
33 pub fn label(self) -> &'static str {
35 match self {
36 Self::Chat => "Chat model",
37 Self::Embedder => "Embedder",
38 Self::Store => "Store",
39 Self::Parser => "Parser",
40 Self::Chunker => "Chunker",
41 Self::Retriever => "Retriever",
42 Self::Memory => "Chat memory",
43 Self::Vision => "Vision",
44 Self::References => "References",
45 Self::Other => "Component",
46 }
47 }
48
49 pub fn icon(self) -> IconName {
51 match self {
52 Self::Chat => IconName::Bot,
53 Self::Embedder => IconName::Cpu,
54 Self::Store => IconName::Database,
55 Self::Parser => IconName::FileText,
56 Self::Chunker => IconName::Pilcrow,
57 Self::Retriever => IconName::Search,
58 Self::Memory => IconName::Brain,
59 Self::Vision => IconName::Eye,
60 Self::References => IconName::BookOpen,
61 Self::Other => IconName::Settings,
62 }
63 }
64}
65
66#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
68pub enum ComponentStatus {
69 #[default]
71 Inactive,
72 Ready,
74 Busy,
76 Error,
78}
79
80impl ComponentStatus {
81 pub fn label(self) -> &'static str {
83 match self {
84 Self::Inactive => "inactive",
85 Self::Ready => "ready",
86 Self::Busy => "busy",
87 Self::Error => "error",
88 }
89 }
90
91 pub fn is_busy(self) -> bool {
93 matches!(self, Self::Busy)
94 }
95}
96
97#[derive(Clone, Debug, PartialEq, Eq)]
104pub struct ComponentToggle {
105 pub id: String,
107 pub label: String,
109 pub enabled: bool,
111 pub detail: Option<String>,
113}
114
115impl ComponentToggle {
116 pub fn new(id: impl Into<String>, label: impl Into<String>, enabled: bool) -> Self {
118 Self {
119 id: id.into(),
120 label: label.into(),
121 enabled,
122 detail: None,
123 }
124 }
125
126 pub fn detail(mut self, detail: impl Into<String>) -> Self {
128 self.detail = Some(detail.into());
129 self
130 }
131}
132
133#[derive(Clone, Debug, PartialEq)]
135pub struct AgentComponent {
136 pub id: String,
138 pub kind: ComponentKind,
140 pub label: String,
142 pub status: ComponentStatus,
144 pub detail: Option<String>,
146 pub icon: Option<ChatIcon>,
148 pub toggles: Vec<ComponentToggle>,
150}
151
152impl AgentComponent {
153 pub fn new(id: impl Into<String>, kind: ComponentKind, label: impl Into<String>) -> Self {
155 Self {
156 id: id.into(),
157 kind,
158 label: label.into(),
159 status: ComponentStatus::default(),
160 detail: None,
161 icon: None,
162 toggles: Vec::new(),
163 }
164 }
165
166 pub fn icon(mut self, icon: impl Into<ChatIcon>) -> Self {
171 self.icon = Some(icon.into());
172 self
173 }
174
175 pub fn status(mut self, status: ComponentStatus) -> Self {
177 self.status = status;
178 self
179 }
180
181 pub fn detail(mut self, detail: impl Into<String>) -> Self {
183 self.detail = Some(detail.into());
184 self
185 }
186
187 pub fn toggle(mut self, toggle: ComponentToggle) -> Self {
189 self.toggles.push(toggle);
190 self
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197
198 #[test]
199 fn kinds_have_labels_and_icons() {
200 assert_eq!(ComponentKind::Chat.label(), "Chat model");
201 assert_eq!(ComponentKind::Store.icon(), IconName::Database);
202 assert_eq!(ComponentKind::Chunker.icon(), IconName::Pilcrow);
203 assert_eq!(ComponentKind::References.label(), "References");
204 assert_eq!(ComponentKind::References.icon(), IconName::BookOpen);
205 }
206
207 #[test]
208 fn every_component_icon_draws_visible_pixels() {
209 use gpui_kit::{AssetSource as _, SvgRenderer};
210 use std::sync::Arc;
211
212 let assets = Arc::new(gpui_kit::assets::AllAssets);
213 let renderer = SvgRenderer::new(assets.clone());
214 let kinds = [
215 ComponentKind::Chat,
216 ComponentKind::Embedder,
217 ComponentKind::Store,
218 ComponentKind::Parser,
219 ComponentKind::Chunker,
220 ComponentKind::Retriever,
221 ComponentKind::Memory,
222 ComponentKind::Vision,
223 ComponentKind::References,
224 ComponentKind::Other,
225 ];
226 for kind in kinds {
227 let path = kind.icon().path();
228 let bytes = assets.load(&path).expect("asset load").expect("asset");
229 let parsed = renderer.parse_svg(&bytes).expect("parse svg");
230 let image = renderer.render_parsed(&parsed, 2.0f32).expect("render svg");
231 let pixels = image.as_bytes(0).expect("frame");
232 assert!(
233 pixels.as_chunks::<4>().0.iter().any(|pixel| pixel[3] != 0),
234 "{:?} ({path}) rasterised to a blank image",
235 kind
236 );
237 }
238 }
239
240 #[test]
241 fn components_default_to_inactive() {
242 let component = AgentComponent::new("chat", ComponentKind::Chat, "DeepSeek");
243 assert_eq!(component.status, ComponentStatus::Inactive);
244 assert!(!component.status.is_busy());
245 assert!(component.toggles.is_empty());
246 }
247
248 #[test]
249 fn a_component_can_carry_switches() {
250 let component = AgentComponent::new("memory", ComponentKind::Memory, "Chat memory")
251 .toggle(ComponentToggle::new("memory", "Enabled", true).detail("rewrite"));
252 assert_eq!(component.toggles.len(), 1);
253 assert_eq!(component.toggles[0].id, "memory");
254 assert!(component.toggles[0].enabled);
255 }
256}