assistant: Require user to accept TOS for cloud provider (#16111)

This adds the requirement for users to accept the terms of service the
first time they send a message with the Cloud provider.

Once this is out and in a nightly, we need to add the check to the
server side too, to authenticate access to the models.

Demo:


https://github.com/user-attachments/assets/0edebf74-8120-4fa2-b801-bb76f04e8a17



Release Notes:

- N/A
This commit is contained in:
Thorsten Ball 2024-08-12 17:43:35 +02:00 committed by GitHub
parent 98f314ba21
commit fbb533b3e0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 297 additions and 9 deletions

View file

@ -9,7 +9,9 @@ pub mod settings;
use anyhow::Result;
use client::{Client, UserStore};
use futures::{future::BoxFuture, stream::BoxStream};
use gpui::{AnyView, AppContext, AsyncAppContext, Model, SharedString, Task, WindowContext};
use gpui::{
AnyElement, AnyView, AppContext, AsyncAppContext, Model, SharedString, Task, WindowContext,
};
pub use model::*;
use project::Fs;
use proto::Plan;
@ -114,6 +116,12 @@ pub trait LanguageModelProvider: 'static {
fn is_authenticated(&self, cx: &AppContext) -> bool;
fn authenticate(&self, cx: &mut AppContext) -> Task<Result<()>>;
fn configuration_view(&self, cx: &mut WindowContext) -> AnyView;
fn must_accept_terms(&self, _cx: &AppContext) -> bool {
false
}
fn render_accept_terms(&self, _cx: &mut WindowContext) -> Option<AnyElement> {
None
}
fn reset_credentials(&self, cx: &mut AppContext) -> Task<Result<()>>;
}

View file

@ -9,7 +9,10 @@ use client::{Client, PerformCompletionParams, UserStore, EXPIRED_LLM_TOKEN_HEADE
use collections::BTreeMap;
use feature_flags::{FeatureFlagAppExt, LanguageModels};
use futures::{future::BoxFuture, stream::BoxStream, AsyncBufReadExt, FutureExt, StreamExt};
use gpui::{AnyView, AppContext, AsyncAppContext, Model, ModelContext, Subscription, Task};
use gpui::{
AnyElement, AnyView, AppContext, AsyncAppContext, FontWeight, Model, ModelContext,
Subscription, Task,
};
use http_client::{AsyncBody, HttpClient, Method, Response};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@ -62,6 +65,7 @@ pub struct State {
client: Arc<Client>,
user_store: Model<UserStore>,
status: client::Status,
accept_terms: Option<Task<Result<()>>>,
_subscription: Subscription,
}
@ -77,6 +81,26 @@ impl State {
this.update(&mut cx, |_, cx| cx.notify())
})
}
fn has_accepted_terms_of_service(&self, cx: &AppContext) -> bool {
self.user_store
.read(cx)
.current_user_has_accepted_terms()
.unwrap_or(false)
}
fn accept_terms_of_service(&mut self, cx: &mut ModelContext<Self>) {
let user_store = self.user_store.clone();
self.accept_terms = Some(cx.spawn(move |this, mut cx| async move {
let _ = user_store
.update(&mut cx, |store, cx| store.accept_terms_of_service(cx))?
.await;
this.update(&mut cx, |this, cx| {
this.accept_terms = None;
cx.notify()
})
}));
}
}
impl CloudLanguageModelProvider {
@ -88,6 +112,7 @@ impl CloudLanguageModelProvider {
client: client.clone(),
user_store,
status,
accept_terms: None,
_subscription: cx.observe_global::<SettingsStore>(|_, cx| {
cx.notify();
}),
@ -223,6 +248,57 @@ impl LanguageModelProvider for CloudLanguageModelProvider {
.into()
}
fn must_accept_terms(&self, cx: &AppContext) -> bool {
!self.state.read(cx).has_accepted_terms_of_service(cx)
}
fn render_accept_terms(&self, cx: &mut WindowContext) -> Option<AnyElement> {
let state = self.state.read(cx);
let terms = [(
"anthropic_terms_of_service",
"Anthropic Terms of Service",
"https://www.anthropic.com/legal/consumer-terms",
)]
.map(|(id, label, url)| {
Button::new(id, label)
.style(ButtonStyle::Subtle)
.icon(IconName::ExternalLink)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.on_click(move |_, cx| cx.open_url(url))
});
if state.has_accepted_terms_of_service(cx) {
None
} else {
let disabled = state.accept_terms.is_some();
Some(
v_flex()
.child(Label::new("Terms & Conditions").weight(FontWeight::SEMIBOLD))
.child("Please read and accept the terms and conditions of Zed AI and our provider partners to continue.")
.child(v_flex().m_2().gap_1().children(terms))
.child(
h_flex().justify_end().mt_1().child(
Button::new("accept_terms", "Accept")
.disabled(disabled)
.on_click({
let state = self.state.downgrade();
move |_, cx| {
state
.update(cx, |state, cx| {
state.accept_terms_of_service(cx)
})
.ok();
}
}),
),
)
.into_any(),
)
}
}
fn reset_credentials(&self, _cx: &mut AppContext) -> Task<Result<()>> {
Task::ready(Ok(()))
}
@ -766,6 +842,7 @@ impl Render for ConfigurationView {
let is_connected = !self.state.read(cx).is_signed_out();
let plan = self.state.read(cx).user_store.read(cx).current_plan();
let must_accept_terms = !self.state.read(cx).has_accepted_terms_of_service(cx);
let is_pro = plan == Some(proto::Plan::ZedPro);
@ -773,6 +850,11 @@ impl Render for ConfigurationView {
v_flex()
.gap_3()
.max_w_4_5()
.when(must_accept_terms, |this| {
this.child(Label::new(
"You must accept the terms of service to use this provider.",
))
})
.child(Label::new(
if is_pro {
"You have full access to Zed's hosted models from Anthropic, OpenAI, Google with faster speeds and higher limits through Zed Pro."