ui/components/ai/
profile_selector.rs1use std::sync::Arc;
2
3use gpui::SharedString;
4
5use crate::prelude::*;
6use crate::{ContextMenu, DropdownMenu, DropdownStyle};
7
8#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct ProfileOption {
12 pub key: SharedString,
13 pub label: SharedString,
14}
15
16impl ProfileOption {
17 pub fn new(key: impl Into<SharedString>, label: impl Into<SharedString>) -> Self {
18 Self {
19 key: key.into(),
20 label: label.into(),
21 }
22 }
23}
24
25impl From<(String, String)> for ProfileOption {
26 fn from((key, label): (String, String)) -> Self {
27 Self::new(key, label)
28 }
29}
30
31impl From<String> for ProfileOption {
32 fn from(key: String) -> Self {
33 let label = key.clone();
34 Self::new(key, label)
35 }
36}
37
38impl From<&str> for ProfileOption {
39 fn from(key: &str) -> Self {
40 Self::new(key, key)
41 }
42}
43
44#[derive(IntoElement, RegisterComponent)]
51pub struct ProfileSelector {
52 id: ElementId,
53 current_key: Option<SharedString>,
54 options: Vec<ProfileOption>,
55 on_select: Option<Arc<dyn Fn(SharedString, &mut Window, &mut App) + 'static>>,
56}
57
58impl ProfileSelector {
59 pub fn new(
60 id: impl Into<ElementId>,
61 current_key: Option<&str>,
62 options: impl IntoIterator<Item = impl Into<ProfileOption>>,
63 ) -> Self {
64 Self {
65 id: id.into(),
66 current_key: current_key.map(SharedString::from),
67 options: options.into_iter().map(Into::into).collect(),
68 on_select: None,
69 }
70 }
71
72 pub fn on_select(
73 mut self,
74 handler: impl Fn(SharedString, &mut Window, &mut App) + 'static,
75 ) -> Self {
76 self.on_select = Some(Arc::new(handler));
77 self
78 }
79}
80
81impl RenderOnce for ProfileSelector {
82 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
83 let current_label = self
84 .current_key
85 .as_ref()
86 .and_then(|key| self.options.iter().find(|option| &option.key == key))
87 .map(|option| option.label.clone());
88 let label = current_label.unwrap_or_else(|| "Select profile".into());
89 let current_key = self.current_key.clone();
90 let on_select = self.on_select.clone();
91 let options = self.options.clone();
92
93 let menu = ContextMenu::build(window, cx, move |mut menu, _window, _cx| {
94 for option in &options {
95 let is_current = current_key.as_ref() == Some(&option.key);
96 let key = option.key.clone();
97 let on_select = on_select.clone();
98 menu = menu.toggleable_entry(
99 option.label.clone(),
100 is_current,
101 IconPosition::End,
102 None,
103 move |window, cx| {
104 if let Some(on_select) = &on_select {
105 on_select(key.clone(), window, cx);
106 }
107 },
108 );
109 }
110 menu
111 });
112
113 DropdownMenu::new(self.id, label, menu).style(DropdownStyle::Subtle)
114 }
115}
116
117impl Component for ProfileSelector {
118 fn scope() -> ComponentScope {
119 ComponentScope::Agent
120 }
121
122 fn name() -> &'static str {
123 "ProfileSelector"
124 }
125
126 fn description() -> Option<&'static str> {
127 Some(
128 "A dropdown for picking an agent's active profile/config option \
129 out of a caller-supplied list of keys, optionally paired with \
130 display labels. No data fetching happens here.",
131 )
132 }
133
134 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
135 let plain_keys = vec!["default", "careful", "yolo"];
136 let labeled_keys = vec![
137 ("default".to_string(), "Default".to_string()),
138 (
139 "careful".to_string(),
140 "Careful (read-only tools)".to_string(),
141 ),
142 ("yolo".to_string(), "Yolo (all tools)".to_string()),
143 ];
144
145 Some(
146 v_flex()
147 .gap_4()
148 .child(single_example(
149 "Plain keys",
150 ProfileSelector::new("profile-selector-1", Some("default"), plain_keys)
151 .into_any_element(),
152 ))
153 .child(single_example(
154 "Key + label pairs",
155 ProfileSelector::new("profile-selector-2", Some("careful"), labeled_keys)
156 .into_any_element(),
157 ))
158 .child(single_example(
159 "No selection",
160 ProfileSelector::new(
161 "profile-selector-3",
162 None,
163 vec!["default", "careful", "yolo"],
164 )
165 .into_any_element(),
166 ))
167 .into_any_element(),
168 )
169 }
170}