1use edit_prediction::{
2 ApiKeyState, MercuryFeatureFlag, SweepFeatureFlag,
3 mercury::{MERCURY_CREDENTIALS_URL, mercury_api_token},
4 sweep_ai::{SWEEP_CREDENTIALS_URL, sweep_api_token},
5};
6use feature_flags::FeatureFlagAppExt as _;
7use gpui::{Entity, ScrollHandle, prelude::*};
8use language_models::provider::mistral::{CODESTRAL_API_URL, codestral_api_key};
9use ui::{ButtonLink, ConfiguredApiCard, prelude::*};
10
11use crate::{
12 SettingField, SettingItem, SettingsFieldMetadata, SettingsPageItem, SettingsWindow, USER,
13 components::{SettingsInputField, SettingsSectionHeader},
14};
15
16pub(crate) fn render_edit_prediction_setup_page(
17 settings_window: &SettingsWindow,
18 scroll_handle: &ScrollHandle,
19 window: &mut Window,
20 cx: &mut Context<SettingsWindow>,
21) -> AnyElement {
22 let providers = [
23 Some(render_github_copilot_provider(window, cx).into_any_element()),
24 cx.has_flag::<MercuryFeatureFlag>().then(|| {
25 render_api_key_provider(
26 IconName::Inception,
27 "Mercury",
28 "https://platform.inceptionlabs.ai/dashboard/api-keys".into(),
29 mercury_api_token(cx),
30 |_cx| MERCURY_CREDENTIALS_URL,
31 None,
32 window,
33 cx,
34 )
35 .into_any_element()
36 }),
37 cx.has_flag::<SweepFeatureFlag>().then(|| {
38 render_api_key_provider(
39 IconName::SweepAi,
40 "Sweep",
41 "https://app.sweep.dev/".into(),
42 sweep_api_token(cx),
43 |_cx| SWEEP_CREDENTIALS_URL,
44 None,
45 window,
46 cx,
47 )
48 .into_any_element()
49 }),
50 Some(
51 render_api_key_provider(
52 IconName::AiMistral,
53 "Codestral",
54 "https://console.mistral.ai/codestral".into(),
55 codestral_api_key(cx),
56 |cx| language_models::MistralLanguageModelProvider::api_url(cx),
57 Some(
58 settings_window
59 .render_sub_page_items_section(
60 codestral_settings().iter().enumerate(),
61 window,
62 cx,
63 )
64 .into_any_element(),
65 ),
66 window,
67 cx,
68 )
69 .into_any_element(),
70 ),
71 ];
72
73 div()
74 .size_full()
75 .child(
76 v_flex()
77 .id("ep-setup-page")
78 .min_w_0()
79 .size_full()
80 .px_8()
81 .pb_16()
82 .overflow_y_scroll()
83 .track_scroll(&scroll_handle)
84 .children(providers.into_iter().flatten()),
85 )
86 .into_any_element()
87}
88
89fn render_api_key_provider(
90 icon: IconName,
91 title: &'static str,
92 link: SharedString,
93 api_key_state: Entity<ApiKeyState>,
94 current_url: fn(&mut App) -> SharedString,
95 additional_fields: Option<AnyElement>,
96 window: &mut Window,
97 cx: &mut Context<SettingsWindow>,
98) -> impl IntoElement {
99 let weak_page = cx.weak_entity();
100 _ = window.use_keyed_state(title, cx, |_, cx| {
101 let task = api_key_state.update(cx, |key_state, cx| {
102 key_state.load_if_needed(current_url(cx), |state| state, cx)
103 });
104 cx.spawn(async move |_, cx| {
105 task.await.ok();
106 weak_page
107 .update(cx, |_, cx| {
108 cx.notify();
109 })
110 .ok();
111 })
112 });
113
114 let (has_key, env_var_name, is_from_env_var) = api_key_state.read_with(cx, |state, _| {
115 (
116 state.has_key(),
117 Some(state.env_var_name().clone()),
118 state.is_from_env_var(),
119 )
120 });
121
122 let write_key = move |api_key: Option<String>, cx: &mut App| {
123 api_key_state
124 .update(cx, |key_state, cx| {
125 let url = current_url(cx);
126 key_state.store(url, api_key, |key_state| key_state, cx)
127 })
128 .detach_and_log_err(cx);
129 };
130
131 let base_container = v_flex().id(title).min_w_0().pt_8().gap_1p5();
132 let header = SettingsSectionHeader::new(title)
133 .icon(icon)
134 .no_padding(true);
135 let button_link_label = format!("{} dashboard", title);
136 let description = h_flex()
137 .min_w_0()
138 .gap_0p5()
139 .child(
140 Label::new("Visit the")
141 .size(LabelSize::Small)
142 .color(Color::Muted),
143 )
144 .child(
145 ButtonLink::new(button_link_label, link)
146 .no_icon(true)
147 .label_size(LabelSize::Small)
148 .label_color(Color::Muted),
149 )
150 .child(
151 Label::new("to generate an API key.")
152 .size(LabelSize::Small)
153 .color(Color::Muted),
154 );
155 let configured_card_label = if is_from_env_var {
156 "API Key Set in Environment Variable"
157 } else {
158 "API Key Configured"
159 };
160
161 let container = if has_key {
162 base_container.child(header).child(
163 ConfiguredApiCard::new(configured_card_label)
164 .button_label("Reset Key")
165 .button_tab_index(0)
166 .disabled(is_from_env_var)
167 .when_some(env_var_name, |this, env_var_name| {
168 this.when(is_from_env_var, |this| {
169 this.tooltip_label(format!(
170 "To reset your API key, unset the {} environment variable.",
171 env_var_name
172 ))
173 })
174 })
175 .on_click(move |_, _, cx| {
176 write_key(None, cx);
177 }),
178 )
179 } else {
180 base_container.child(header).child(
181 h_flex()
182 .pt_2p5()
183 .w_full()
184 .justify_between()
185 .child(
186 v_flex()
187 .w_full()
188 .max_w_1_2()
189 .child(Label::new("API Key"))
190 .child(description)
191 .when_some(env_var_name, |this, env_var_name| {
192 this.child({
193 let label = format!(
194 "Or set the {} env var and restart Zed.",
195 env_var_name.as_ref()
196 );
197 Label::new(label).size(LabelSize::Small).color(Color::Muted)
198 })
199 }),
200 )
201 .child(
202 SettingsInputField::new()
203 .tab_index(0)
204 .with_placeholder("xxxxxxxxxxxxxxxxxxxx")
205 .on_confirm(move |api_key, cx| {
206 write_key(api_key.filter(|key| !key.is_empty()), cx);
207 }),
208 ),
209 )
210 };
211
212 container.when_some(additional_fields, |this, additional_fields| {
213 this.child(
214 div()
215 .map(|this| if has_key { this.mt_1() } else { this.mt_4() })
216 .px_neg_8()
217 .border_t_1()
218 .border_color(cx.theme().colors().border_variant)
219 .child(additional_fields),
220 )
221 })
222}
223
224fn codestral_settings() -> Box<[SettingsPageItem]> {
225 Box::new([
226 SettingsPageItem::SettingItem(SettingItem {
227 title: "API URL",
228 description: "The API URL to use for Codestral.",
229 field: Box::new(SettingField {
230 pick: |settings| {
231 settings
232 .project
233 .all_languages
234 .edit_predictions
235 .as_ref()?
236 .codestral
237 .as_ref()?
238 .api_url
239 .as_ref()
240 },
241 write: |settings, value| {
242 settings
243 .project
244 .all_languages
245 .edit_predictions
246 .get_or_insert_default()
247 .codestral
248 .get_or_insert_default()
249 .api_url = value;
250 },
251 json_path: Some("edit_predictions.codestral.api_url"),
252 }),
253 metadata: Some(Box::new(SettingsFieldMetadata {
254 placeholder: Some(CODESTRAL_API_URL),
255 ..Default::default()
256 })),
257 files: USER,
258 }),
259 SettingsPageItem::SettingItem(SettingItem {
260 title: "Max Tokens",
261 description: "The maximum number of tokens to generate.",
262 field: Box::new(SettingField {
263 pick: |settings| {
264 settings
265 .project
266 .all_languages
267 .edit_predictions
268 .as_ref()?
269 .codestral
270 .as_ref()?
271 .max_tokens
272 .as_ref()
273 },
274 write: |settings, value| {
275 settings
276 .project
277 .all_languages
278 .edit_predictions
279 .get_or_insert_default()
280 .codestral
281 .get_or_insert_default()
282 .max_tokens = value;
283 },
284 json_path: Some("edit_predictions.codestral.max_tokens"),
285 }),
286 metadata: None,
287 files: USER,
288 }),
289 SettingsPageItem::SettingItem(SettingItem {
290 title: "Model",
291 description: "The Codestral model id to use.",
292 field: Box::new(SettingField {
293 pick: |settings| {
294 settings
295 .project
296 .all_languages
297 .edit_predictions
298 .as_ref()?
299 .codestral
300 .as_ref()?
301 .model
302 .as_ref()
303 },
304 write: |settings, value| {
305 settings
306 .project
307 .all_languages
308 .edit_predictions
309 .get_or_insert_default()
310 .codestral
311 .get_or_insert_default()
312 .model = value;
313 },
314 json_path: Some("edit_predictions.codestral.model"),
315 }),
316 metadata: Some(Box::new(SettingsFieldMetadata {
317 placeholder: Some("codestral-latest"),
318 ..Default::default()
319 })),
320 files: USER,
321 }),
322 ])
323}
324
325pub(crate) fn render_github_copilot_provider(
326 window: &mut Window,
327 cx: &mut App,
328) -> impl IntoElement {
329 let configuration_view = window.use_state(cx, |_, cx| {
330 copilot::ConfigurationView::new(
331 |cx| {
332 copilot::Copilot::global(cx)
333 .is_some_and(|copilot| copilot.read(cx).is_authenticated())
334 },
335 copilot::ConfigurationMode::EditPrediction,
336 cx,
337 )
338 });
339
340 v_flex()
341 .id("github-copilot")
342 .min_w_0()
343 .gap_1p5()
344 .child(
345 SettingsSectionHeader::new("GitHub Copilot")
346 .icon(IconName::Copilot)
347 .no_padding(true),
348 )
349 .child(configuration_view)
350}