155 lines · 5.6 KB · Rust
RawDownload
use std::rc::Rc; use anyhow::Result;use gpui::{App, Context, Entity, Subscription, Task, Window};use language_model::ApiKeyState;use ui::{Tooltip, prelude::*};use ui_input::InputField; /// The current credential state of a single-API-key provider, as reported by the/// provider when constructing an [`ApiKeyEditor`].pub enum ApiKeyStatus {    /// No key is configured; show the input field.    Unset,    /// A key is configured via the UI; show a "configured" row with a reset.    Configured,    /// The key comes from an environment variable and can't be edited here.    FromEnvVar(SharedString),} /// Maps a provider's [`ApiKeyState`] to the [`ApiKeyStatus`] the editor renders./// Shared so the API-key providers don't each duplicate this mapping.pub fn api_key_status(state: &ApiKeyState) -> ApiKeyStatus {    if state.is_from_env_var() {        ApiKeyStatus::FromEnvVar(state.env_var_name().clone())    } else if state.has_key() {        ApiKeyStatus::Configured    } else {        ApiKeyStatus::Unset    }} /// A compact, reusable control for editing a provider's single API key, intended/// to be returned from `LanguageModelProvider::configuration_view_v2` as an/// inline control.////// It is deliberately provider-agnostic: the provider supplies closures that/// read the current [`ApiKeyStatus`] and store/clear the key against its own/// state, so all credential knowledge stays in the provider.pub struct ApiKeyEditor {    input: Entity<InputField>,    api_key_url: SharedString,    status: Rc<dyn Fn(&App) -> ApiKeyStatus>,    set_key: Rc<dyn Fn(String, &mut App) -> Task<Result<()>>>,    reset_key: Rc<dyn Fn(&mut App) -> Task<Result<()>>>,    _subscription: Subscription,} impl ApiKeyEditor {    pub fn new<S: 'static>(        state: Entity<S>,        api_key_url: impl Into<SharedString>,        placeholder: &str,        status: impl Fn(&S, &App) -> ApiKeyStatus + 'static,        set_key: impl Fn(&Entity<S>, String, &mut App) -> Task<Result<()>> + 'static,        reset_key: impl Fn(&Entity<S>, &mut App) -> Task<Result<()>> + 'static,        window: &mut Window,        cx: &mut Context<Self>,    ) -> Self {        let input = cx.new(|cx| {            InputField::new(window, cx, placeholder)                .masked(true)                .tab_index(0)        });        let subscription = cx.observe(&state, |_, _, cx| cx.notify());         let status_state = state.clone();        let set_state = state.clone();        Self {            input,            api_key_url: api_key_url.into(),            status: Rc::new(move |cx| status(status_state.read(cx), cx)),            set_key: Rc::new(move |key, cx| set_key(&set_state, key, cx)),            reset_key: Rc::new(move |cx| reset_key(&state, cx)),            _subscription: subscription,        }    }     fn save(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {        let key = self.input.read(cx).text(cx).trim().to_string();        if key.is_empty() {            return;        }        self.input            .update(cx, |input, cx| input.set_text("", window, cx));        (self.set_key.clone())(key, cx).detach_and_log_err(cx);    }     fn reset(&mut self, cx: &mut Context<Self>) {        (self.reset_key.clone())(cx).detach_and_log_err(cx);    }     fn render_where_to_find_key(&self) -> impl IntoElement {        let url = self.api_key_url.clone();        let click_url = url.to_string();        h_flex()            .id("where-to-find-key")            .gap_0p5()            .cursor_pointer()            .child(                Icon::new(IconName::Info)                    .size(IconSize::XSmall)                    .color(Color::Muted),            )            .child(                Label::new("Where to find key")                    .size(LabelSize::Small)                    .color(Color::Muted),            )            .tooltip(Tooltip::text(format!("Create an API key at {url}")))            .on_click(move |_, _window, cx| cx.open_url(&click_url))    }} impl Render for ApiKeyEditor {    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {        match (self.status)(cx) {            ApiKeyStatus::FromEnvVar(env_var_name) => Label::new(format!("Set via {env_var_name}"))                .size(LabelSize::Small)                .color(Color::Muted)                .into_any_element(),            ApiKeyStatus::Configured => h_flex()                .gap_2()                .items_center()                .child(                    Icon::new(IconName::Check)                        .size(IconSize::Small)                        .color(Color::Success),                )                .child(                    Label::new("Configured")                        .size(LabelSize::Small)                        .color(Color::Muted),                )                .child(                    Button::new("reset-api-key", "Reset")                        .style(ButtonStyle::Outlined)                        .label_size(LabelSize::Small)                        .tab_index(0isize)                        .on_click(cx.listener(|this, _, _window, cx| this.reset(cx))),                )                .into_any_element(),            ApiKeyStatus::Unset => v_flex()                .w_full()                .gap_1()                .child(self.render_where_to_find_key())                .child(                    div()                        .w_full()                        .on_action(cx.listener(Self::save))                        .child(self.input.clone()),                )                .into_any_element(),        }    }}