use gpui_kit::assets::IconName;
use crate::icon::ChatIcon;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ComponentKind {
Chat,
Embedder,
Store,
Parser,
Chunker,
Retriever,
Memory,
Vision,
References,
Other,
}
impl ComponentKind {
pub fn label(self) -> &'static str {
match self {
Self::Chat => "Chat model",
Self::Embedder => "Embedder",
Self::Store => "Store",
Self::Parser => "Parser",
Self::Chunker => "Chunker",
Self::Retriever => "Retriever",
Self::Memory => "Chat memory",
Self::Vision => "Vision",
Self::References => "References",
Self::Other => "Component",
}
}
pub fn icon(self) -> IconName {
match self {
Self::Chat => IconName::Bot,
Self::Embedder => IconName::Cpu,
Self::Store => IconName::Database,
Self::Parser => IconName::FileText,
Self::Chunker => IconName::Pilcrow,
Self::Retriever => IconName::Search,
Self::Memory => IconName::Brain,
Self::Vision => IconName::Eye,
Self::References => IconName::BookOpen,
Self::Other => IconName::Settings,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum ComponentStatus {
#[default]
Inactive,
Ready,
Busy,
Error,
}
impl ComponentStatus {
pub fn label(self) -> &'static str {
match self {
Self::Inactive => "inactive",
Self::Ready => "ready",
Self::Busy => "busy",
Self::Error => "error",
}
}
pub fn is_busy(self) -> bool {
matches!(self, Self::Busy)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ComponentToggle {
pub id: String,
pub label: String,
pub enabled: bool,
pub detail: Option<String>,
}
impl ComponentToggle {
pub fn new(id: impl Into<String>, label: impl Into<String>, enabled: bool) -> Self {
Self {
id: id.into(),
label: label.into(),
enabled,
detail: None,
}
}
pub fn detail(mut self, detail: impl Into<String>) -> Self {
self.detail = Some(detail.into());
self
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct AgentComponent {
pub id: String,
pub kind: ComponentKind,
pub label: String,
pub status: ComponentStatus,
pub detail: Option<String>,
pub icon: Option<ChatIcon>,
pub toggles: Vec<ComponentToggle>,
}
impl AgentComponent {
pub fn new(id: impl Into<String>, kind: ComponentKind, label: impl Into<String>) -> Self {
Self {
id: id.into(),
kind,
label: label.into(),
status: ComponentStatus::default(),
detail: None,
icon: None,
toggles: Vec::new(),
}
}
pub fn icon(mut self, icon: impl Into<ChatIcon>) -> Self {
self.icon = Some(icon.into());
self
}
pub fn status(mut self, status: ComponentStatus) -> Self {
self.status = status;
self
}
pub fn detail(mut self, detail: impl Into<String>) -> Self {
self.detail = Some(detail.into());
self
}
pub fn toggle(mut self, toggle: ComponentToggle) -> Self {
self.toggles.push(toggle);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn kinds_have_labels_and_icons() {
assert_eq!(ComponentKind::Chat.label(), "Chat model");
assert_eq!(ComponentKind::Store.icon(), IconName::Database);
assert_eq!(ComponentKind::Chunker.icon(), IconName::Pilcrow);
assert_eq!(ComponentKind::References.label(), "References");
assert_eq!(ComponentKind::References.icon(), IconName::BookOpen);
}
#[test]
fn every_component_icon_draws_visible_pixels() {
use gpui_kit::{AssetSource as _, SvgRenderer};
use std::sync::Arc;
let assets = Arc::new(gpui_kit::assets::AllAssets);
let renderer = SvgRenderer::new(assets.clone());
let kinds = [
ComponentKind::Chat,
ComponentKind::Embedder,
ComponentKind::Store,
ComponentKind::Parser,
ComponentKind::Chunker,
ComponentKind::Retriever,
ComponentKind::Memory,
ComponentKind::Vision,
ComponentKind::References,
ComponentKind::Other,
];
for kind in kinds {
let path = kind.icon().path();
let bytes = assets.load(&path).expect("asset load").expect("asset");
let parsed = renderer.parse_svg(&bytes).expect("parse svg");
let image = renderer.render_parsed(&parsed, 2.0f32).expect("render svg");
let pixels = image.as_bytes(0).expect("frame");
assert!(
pixels.as_chunks::<4>().0.iter().any(|pixel| pixel[3] != 0),
"{:?} ({path}) rasterised to a blank image",
kind
);
}
}
#[test]
fn components_default_to_inactive() {
let component = AgentComponent::new("chat", ComponentKind::Chat, "DeepSeek");
assert_eq!(component.status, ComponentStatus::Inactive);
assert!(!component.status.is_busy());
assert!(component.toggles.is_empty());
}
#[test]
fn a_component_can_carry_switches() {
let component = AgentComponent::new("memory", ComponentKind::Memory, "Chat memory")
.toggle(ComponentToggle::new("memory", "Enabled", true).detail("rewrite"));
assert_eq!(component.toggles.len(), 1);
assert_eq!(component.toggles[0].id, "memory");
assert!(component.toggles[0].enabled);
}
}