edit_prediction_provider_setup.rs

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