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