468 lines · 15.3 KB · Rust
RawDownload
use ai_onboarding::YoungAccountBanner;use anyhow::Result;use client::{Client, RefreshLlmTokenListener, UserStore, global_llm_token, zed_urls};use cloud_api_client::LlmApiToken;use cloud_api_types::OrganizationId;use cloud_api_types::Plan;use futures::StreamExt;use futures::future::BoxFuture;use gpui::AsyncApp;use gpui::{AnyElement, AnyView, App, Context, Entity, Subscription, Task};use language_model::{    AuthenticateError, IconOrSvg, LanguageModel, LanguageModelProvider, LanguageModelProviderId,    LanguageModelProviderName, LanguageModelProviderState, ZED_CLOUD_PROVIDER_ID,    ZED_CLOUD_PROVIDER_NAME,};use language_models_cloud::{CloudLlmTokenProvider, CloudModelProvider};use release_channel::AppVersion; use settings::SettingsStore;pub use settings::ZedDotDevAvailableModel as AvailableModel;pub use settings::ZedDotDevAvailableProvider as AvailableProvider;use std::sync::Arc;use ui::{TintColor, prelude::*}; const PROVIDER_ID: LanguageModelProviderId = ZED_CLOUD_PROVIDER_ID;const PROVIDER_NAME: LanguageModelProviderName = ZED_CLOUD_PROVIDER_NAME; struct ClientTokenProvider {    client: Arc<Client>,    llm_api_token: LlmApiToken,    user_store: Entity<UserStore>,} impl CloudLlmTokenProvider for ClientTokenProvider {    type AuthContext = Option<OrganizationId>;     fn auth_context(&self, cx: &AsyncApp) -> Self::AuthContext {        self.user_store.read_with(cx, |user_store, _| {            user_store                .current_organization()                .map(|organization| organization.id.clone())        })    }     fn acquire_token(        &self,        organization_id: Self::AuthContext,    ) -> BoxFuture<'static, Result<String>> {        let client = self.client.clone();        let llm_api_token = self.llm_api_token.clone();        Box::pin(async move {            client                .acquire_llm_token(&llm_api_token, organization_id)                .await        })    }     fn refresh_token(        &self,        organization_id: Self::AuthContext,    ) -> BoxFuture<'static, Result<String>> {        let client = self.client.clone();        let llm_api_token = self.llm_api_token.clone();        Box::pin(async move {            client                .refresh_llm_token(&llm_api_token, organization_id)                .await        })    }} #[derive(Default, Clone, Debug, PartialEq)]pub struct ZedDotDevSettings {    pub available_models: Vec<AvailableModel>,} pub struct CloudLanguageModelProvider {    state: Entity<State>,    _maintain_client_status: Task<()>,} pub struct State {    client: Arc<Client>,    user_store: Entity<UserStore>,    status: client::Status,    provider: Entity<CloudModelProvider<ClientTokenProvider>>,    _user_store_subscription: Subscription,    _settings_subscription: Subscription,    _llm_token_subscription: Subscription,    _provider_subscription: Subscription,} impl State {    fn new(        client: Arc<Client>,        user_store: Entity<UserStore>,        status: client::Status,        cx: &mut Context<Self>,    ) -> Self {        let refresh_llm_token_listener = RefreshLlmTokenListener::global(cx);        let token_provider = Arc::new(ClientTokenProvider {            client: client.clone(),            llm_api_token: global_llm_token(cx),            user_store: user_store.clone(),        });         let provider = cx.new(|cx| {            CloudModelProvider::new(                token_provider.clone(),                client.http_client(),                Some(AppVersion::global(cx)),            )        });         Self {            client: client.clone(),            user_store: user_store.clone(),            status,            _provider_subscription: cx.observe(&provider, |_, _, cx| cx.notify()),            provider,            _user_store_subscription: cx.subscribe(                &user_store,                move |this, _user_store, event, cx| match event {                    client::user::Event::PrivateUserInfoUpdated => {                        let status = *client.status().borrow();                        if status.is_signed_out() {                            return;                        }                         this.refresh_models(cx);                    }                    _ => {}                },            ),            _settings_subscription: cx.observe_global::<SettingsStore>(|_, cx| {                cx.notify();            }),            _llm_token_subscription: cx.subscribe(                &refresh_llm_token_listener,                move |this, _listener, _event, cx| {                    this.refresh_models(cx);                },            ),        }    }     fn is_signed_out(&self, cx: &App) -> bool {        self.user_store.read(cx).current_user().is_none()    }     fn authenticate(&self, cx: &mut Context<Self>) -> Task<Result<()>> {        let client = self.client.clone();        cx.spawn(async move |state, cx| {            client.sign_in_with_optional_connect(true, cx).await?;            state.update(cx, |_, cx| cx.notify())        })    }     fn refresh_models(&mut self, cx: &mut Context<Self>) {        self.provider.update(cx, |provider, cx| {            provider.refresh_models(cx).detach_and_log_err(cx);        });    }} impl CloudLanguageModelProvider {    pub fn new(user_store: Entity<UserStore>, client: Arc<Client>, cx: &mut App) -> Self {        let mut status_rx = client.status();        let status = *status_rx.borrow();         let state = cx.new(|cx| State::new(client.clone(), user_store.clone(), status, cx));         let state_ref = state.downgrade();        let maintain_client_status = cx.spawn(async move |cx| {            while let Some(status) = status_rx.next().await {                if let Some(this) = state_ref.upgrade() {                    _ = this.update(cx, |this, cx| {                        if this.status != status {                            this.status = status;                            cx.notify();                        }                    });                } else {                    break;                }            }        });         Self {            state,            _maintain_client_status: maintain_client_status,        }    }} impl LanguageModelProviderState for CloudLanguageModelProvider {    type ObservableEntity = State;     fn observable_entity(&self) -> Option<Entity<Self::ObservableEntity>> {        Some(self.state.clone())    }} impl LanguageModelProvider for CloudLanguageModelProvider {    fn id(&self) -> LanguageModelProviderId {        PROVIDER_ID    }     fn name(&self) -> LanguageModelProviderName {        PROVIDER_NAME    }     fn icon(&self) -> IconOrSvg {        IconOrSvg::Icon(IconName::AiZed)    }     fn default_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> {        let state = self.state.read(cx);        let provider = state.provider.read(cx);        let model = provider.default_model()?;        Some(provider.create_model(model))    }     fn default_fast_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> {        let state = self.state.read(cx);        let provider = state.provider.read(cx);        let model = provider.default_fast_model()?;        Some(provider.create_model(model))    }     fn recommended_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> {        let state = self.state.read(cx);        let provider = state.provider.read(cx);        provider            .recommended_models()            .iter()            .map(|model| provider.create_model(model))            .collect()    }     fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> {        let state = self.state.read(cx);        let provider = state.provider.read(cx);        provider            .models()            .iter()            .map(|model| provider.create_model(model))            .collect()    }     fn is_authenticated(&self, cx: &App) -> bool {        let state = self.state.read(cx);        !state.is_signed_out(cx)    }     fn authenticate(&self, _cx: &mut App) -> Task<Result<(), AuthenticateError>> {        Task::ready(Ok(()))    }     fn configuration_view(        &self,        _target_agent: language_model::ConfigurationViewTargetAgent,        _: &mut Window,        cx: &mut App,    ) -> AnyView {        cx.new(|_| ConfigurationView::new(self.state.clone()))            .into()    }     fn reset_credentials(&self, _cx: &mut App) -> Task<Result<()>> {        Task::ready(Ok(()))    }} #[derive(IntoElement, RegisterComponent)]struct ZedAiConfiguration {    is_connected: bool,    plan: Option<Plan>,    eligible_for_trial: bool,    account_too_young: bool,    sign_in_callback: Arc<dyn Fn(&mut Window, &mut App) + Send + Sync>,} impl RenderOnce for ZedAiConfiguration {    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {        let (subscription_text, has_paid_plan) = match self.plan {            Some(Plan::ZedPro) => (                "You have access to Zed's hosted models through your Pro subscription.",                true,            ),            Some(Plan::ZedProTrial) => (                "You have access to Zed's hosted models through your Pro trial.",                false,            ),            Some(Plan::ZedStudent) => (                "You have access to Zed's hosted models through your Student subscription.",                true,            ),            Some(Plan::ZedBusiness) => (                "You have access to Zed's hosted models through your Organization.",                true,            ),            Some(Plan::ZedFree) | None => (                if self.eligible_for_trial {                    "Subscribe for access to Zed's hosted models. Start with a 14 day free trial."                } else {                    "Subscribe for access to Zed's hosted models."                },                false,            ),        };         let manage_subscription_buttons = if has_paid_plan {            Button::new("manage_settings", "Manage Subscription")                .full_width()                .label_size(LabelSize::Small)                .style(ButtonStyle::Tinted(TintColor::Accent))                .on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx)))                .into_any_element()        } else if self.plan.is_none() || self.eligible_for_trial {            Button::new("start_trial", "Start 14-day Free Pro Trial")                .full_width()                .style(ui::ButtonStyle::Tinted(ui::TintColor::Accent))                .on_click(|_, _, cx| cx.open_url(&zed_urls::start_trial_url(cx)))                .into_any_element()        } else {            Button::new("upgrade", "Upgrade to Pro")                .full_width()                .style(ui::ButtonStyle::Tinted(ui::TintColor::Accent))                .on_click(|_, _, cx| cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx)))                .into_any_element()        };         if !self.is_connected {            return v_flex()                .gap_2()                .child(Label::new("Sign in to have access to Zed's complete agentic experience with hosted models."))                .child(                    Button::new("sign_in", "Sign In to use Zed AI")                        .start_icon(Icon::new(IconName::Github).size(IconSize::Small).color(Color::Muted))                        .full_width()                        .on_click({                            let callback = self.sign_in_callback.clone();                            move |_, window, cx| (callback)(window, cx)                        }),                );        }         v_flex().gap_2().w_full().map(|this| {            if self.account_too_young {                this.child(YoungAccountBanner).child(                    Button::new("upgrade", "Upgrade to Pro")                        .style(ui::ButtonStyle::Tinted(ui::TintColor::Accent))                        .full_width()                        .on_click(|_, _, cx| cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))),                )            } else {                this.text_sm()                    .child(subscription_text)                    .child(manage_subscription_buttons)            }        })    }} struct ConfigurationView {    state: Entity<State>,    sign_in_callback: Arc<dyn Fn(&mut Window, &mut App) + Send + Sync>,} impl ConfigurationView {    fn new(state: Entity<State>) -> Self {        let sign_in_callback = Arc::new({            let state = state.clone();            move |_window: &mut Window, cx: &mut App| {                state.update(cx, |state, cx| {                    state.authenticate(cx).detach_and_log_err(cx);                });            }        });         Self {            state,            sign_in_callback,        }    }} impl Render for ConfigurationView {    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {        let state = self.state.read(cx);        let user_store = state.user_store.read(cx);         ZedAiConfiguration {            is_connected: !state.is_signed_out(cx),            plan: user_store.plan(),            eligible_for_trial: user_store.trial_started_at().is_none(),            account_too_young: user_store.account_too_young(),            sign_in_callback: self.sign_in_callback.clone(),        }    }} impl Component for ZedAiConfiguration {    fn name() -> &'static str {        "AI Configuration Content"    }     fn sort_name() -> &'static str {        "AI Configuration Content"    }     fn scope() -> ComponentScope {        ComponentScope::Onboarding    }     fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {        fn configuration(            is_connected: bool,            plan: Option<Plan>,            eligible_for_trial: bool,            account_too_young: bool,        ) -> AnyElement {            ZedAiConfiguration {                is_connected,                plan,                eligible_for_trial,                account_too_young,                sign_in_callback: Arc::new(|_, _| {}),            }            .into_any_element()        }         Some(            v_flex()                .p_4()                .gap_4()                .children(vec![                    single_example("Not connected", configuration(false, None, false, false)),                    single_example(                        "Accept Terms of Service",                        configuration(true, None, true, false),                    ),                    single_example(                        "No Plan - Not eligible for trial",                        configuration(true, None, false, false),                    ),                    single_example(                        "No Plan - Eligible for trial",                        configuration(true, None, true, false),                    ),                    single_example(                        "Free Plan",                        configuration(true, Some(Plan::ZedFree), true, false),                    ),                    single_example(                        "Zed Pro Trial Plan",                        configuration(true, Some(Plan::ZedProTrial), true, false),                    ),                    single_example(                        "Zed Pro Plan",                        configuration(true, Some(Plan::ZedPro), true, false),                    ),                ])                .into_any_element(),        )    }}