edit_prediction_provider_setup.rs

  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}