assistant: Add a Configuration page (#15490)
- [x] bug: setting a key doesn't update anything - [x] show high-level text on configuration page to explain what it is - [x] show "everything okay!" status when credentials are set - [x] maybe: add "verify" button to check credentials - [x] open configuration page when opening panel for first time and nothing is configured - [x] BUG: need to fix empty assistant panel if provider is `zed.dev` but not logged in Co-Authored-By: Thorsten <thorsten@zed.dev> Release Notes: - N/A --------- Co-authored-by: Thorsten <thorsten@zed.dev> Co-authored-by: Nate Butler <iamnbutler@gmail.com> Co-authored-by: Thorsten Ball <mrnugget@gmail.com>
This commit is contained in:
parent
79213637e2
commit
be3a8584ff
13 changed files with 934 additions and 476 deletions
|
@ -8,8 +8,8 @@ use collections::BTreeMap;
|
|||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
|
||||
use gpui::{
|
||||
AnyView, AppContext, AsyncAppContext, FontStyle, Subscription, Task, TextStyle, View,
|
||||
WhiteSpace,
|
||||
AnyView, AppContext, AsyncAppContext, FocusHandle, FocusableView, FontStyle, ModelContext,
|
||||
Subscription, Task, TextStyle, View, WhiteSpace,
|
||||
};
|
||||
use http_client::HttpClient;
|
||||
use schemars::JsonSchema;
|
||||
|
@ -18,8 +18,7 @@ use settings::{Settings, SettingsStore};
|
|||
use std::{sync::Arc, time::Duration};
|
||||
use strum::IntoEnumIterator;
|
||||
use theme::ThemeSettings;
|
||||
use ui::prelude::*;
|
||||
use util::ResultExt;
|
||||
use ui::{prelude::*, Indicator};
|
||||
|
||||
const PROVIDER_ID: &str = "anthropic";
|
||||
const PROVIDER_NAME: &str = "Anthropic";
|
||||
|
@ -49,6 +48,43 @@ pub struct State {
|
|||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn reset_api_key(&self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
let delete_credentials =
|
||||
cx.delete_credentials(&AllLanguageModelSettings::get_global(cx).anthropic.api_url);
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
delete_credentials.await.ok();
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.api_key = None;
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn set_api_key(&mut self, api_key: String, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
let write_credentials = cx.write_credentials(
|
||||
AllLanguageModelSettings::get_global(cx)
|
||||
.anthropic
|
||||
.api_url
|
||||
.as_str(),
|
||||
"Bearer",
|
||||
api_key.as_bytes(),
|
||||
);
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
write_credentials.await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.api_key = Some(api_key);
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn is_authenticated(&self) -> bool {
|
||||
self.api_key.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
impl AnthropicLanguageModelProvider {
|
||||
pub fn new(http_client: Arc<dyn HttpClient>, cx: &mut AppContext) -> Self {
|
||||
let state = cx.new_model(|cx| State {
|
||||
|
@ -120,7 +156,7 @@ impl LanguageModelProvider for AnthropicLanguageModelProvider {
|
|||
}
|
||||
|
||||
fn is_authenticated(&self, cx: &AppContext) -> bool {
|
||||
self.state.read(cx).api_key.is_some()
|
||||
self.state.read(cx).is_authenticated()
|
||||
}
|
||||
|
||||
fn authenticate(&self, cx: &mut AppContext) -> Task<Result<()>> {
|
||||
|
@ -151,22 +187,14 @@ impl LanguageModelProvider for AnthropicLanguageModelProvider {
|
|||
}
|
||||
}
|
||||
|
||||
fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView {
|
||||
cx.new_view(|cx| AuthenticationPrompt::new(self.state.clone(), cx))
|
||||
.into()
|
||||
fn configuration_view(&self, cx: &mut WindowContext) -> (AnyView, Option<FocusHandle>) {
|
||||
let view = cx.new_view(|cx| ConfigurationView::new(self.state.clone(), cx));
|
||||
let focus_handle = view.focus_handle(cx);
|
||||
(view.into(), Some(focus_handle))
|
||||
}
|
||||
|
||||
fn reset_credentials(&self, cx: &mut AppContext) -> Task<Result<()>> {
|
||||
let state = self.state.clone();
|
||||
let delete_credentials =
|
||||
cx.delete_credentials(&AllLanguageModelSettings::get_global(cx).anthropic.api_url);
|
||||
cx.spawn(|mut cx| async move {
|
||||
delete_credentials.await.log_err();
|
||||
state.update(&mut cx, |this, cx| {
|
||||
this.api_key = None;
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
self.state.update(cx, |state, cx| state.reset_api_key(cx))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -350,18 +378,24 @@ impl LanguageModel for AnthropicModel {
|
|||
}
|
||||
}
|
||||
|
||||
struct AuthenticationPrompt {
|
||||
api_key: View<Editor>,
|
||||
struct ConfigurationView {
|
||||
api_key_editor: View<Editor>,
|
||||
state: gpui::Model<State>,
|
||||
}
|
||||
|
||||
impl AuthenticationPrompt {
|
||||
impl FocusableView for ConfigurationView {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
self.api_key_editor.read(cx).focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl ConfigurationView {
|
||||
fn new(state: gpui::Model<State>, cx: &mut WindowContext) -> Self {
|
||||
Self {
|
||||
api_key: cx.new_view(|cx| {
|
||||
api_key_editor: cx.new_view(|cx| {
|
||||
let mut editor = Editor::single_line(cx);
|
||||
editor.set_placeholder_text(
|
||||
"sk-000000000000000000000000000000000000000000000000",
|
||||
"sk-ant-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||
cx,
|
||||
);
|
||||
editor
|
||||
|
@ -371,29 +405,22 @@ impl AuthenticationPrompt {
|
|||
}
|
||||
|
||||
fn save_api_key(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
|
||||
let api_key = self.api_key.read(cx).text(cx);
|
||||
let api_key = self.api_key_editor.read(cx).text(cx);
|
||||
if api_key.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let write_credentials = cx.write_credentials(
|
||||
AllLanguageModelSettings::get_global(cx)
|
||||
.anthropic
|
||||
.api_url
|
||||
.as_str(),
|
||||
"Bearer",
|
||||
api_key.as_bytes(),
|
||||
);
|
||||
let state = self.state.clone();
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
write_credentials.await?;
|
||||
self.state
|
||||
.update(cx, |state, cx| state.set_api_key(api_key, cx))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
state.update(&mut cx, |this, cx| {
|
||||
this.api_key = Some(api_key);
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
fn reset_api_key(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.api_key_editor
|
||||
.update(cx, |editor, cx| editor.set_text("", cx));
|
||||
self.state
|
||||
.update(cx, |state, cx| state.reset_api_key(cx))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn render_api_key_editor(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
|
@ -413,7 +440,7 @@ impl AuthenticationPrompt {
|
|||
white_space: WhiteSpace::Normal,
|
||||
};
|
||||
EditorElement::new(
|
||||
&self.api_key,
|
||||
&self.api_key_editor,
|
||||
EditorStyle {
|
||||
background: cx.theme().colors().editor_background,
|
||||
local_player: cx.theme().players().local(),
|
||||
|
@ -424,7 +451,7 @@ impl AuthenticationPrompt {
|
|||
}
|
||||
}
|
||||
|
||||
impl Render for AuthenticationPrompt {
|
||||
impl Render for ConfigurationView {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
const INSTRUCTIONS: [&str; 4] = [
|
||||
"To use the assistant panel or inline assistant, you need to add your Anthropic API key.",
|
||||
|
@ -433,38 +460,48 @@ impl Render for AuthenticationPrompt {
|
|||
"Paste your Anthropic API key below and hit enter to use the assistant:",
|
||||
];
|
||||
|
||||
v_flex()
|
||||
.p_4()
|
||||
.size_full()
|
||||
.on_action(cx.listener(Self::save_api_key))
|
||||
.children(
|
||||
INSTRUCTIONS.map(|instruction| Label::new(instruction).size(LabelSize::Small)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.my_2()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.rounded_md()
|
||||
.child(self.render_api_key_editor(cx)),
|
||||
)
|
||||
.child(
|
||||
Label::new(
|
||||
"You can also assign the ANTHROPIC_API_KEY environment variable and restart Zed.",
|
||||
if self.state.read(cx).is_authenticated() {
|
||||
h_flex()
|
||||
.size_full()
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Indicator::dot().color(Color::Success))
|
||||
.child(Label::new("API Key configured").size(LabelSize::Small)),
|
||||
)
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Label::new("Click on").size(LabelSize::Small))
|
||||
.child(Icon::new(IconName::ZedAssistant).size(IconSize::XSmall))
|
||||
.child(
|
||||
Label::new("in the status bar to close this panel.").size(LabelSize::Small),
|
||||
),
|
||||
)
|
||||
.into_any()
|
||||
.child(
|
||||
Button::new("reset-key", "Reset key")
|
||||
.icon(Some(IconName::Trash))
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_position(IconPosition::Start)
|
||||
.on_click(cx.listener(|this, _, cx| this.reset_api_key(cx))),
|
||||
)
|
||||
.into_any()
|
||||
} else {
|
||||
v_flex()
|
||||
.size_full()
|
||||
.on_action(cx.listener(Self::save_api_key))
|
||||
.children(
|
||||
INSTRUCTIONS.map(|instruction| Label::new(instruction).size(LabelSize::Small)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.my_2()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.rounded_md()
|
||||
.child(self.render_api_key_editor(cx)),
|
||||
)
|
||||
.child(
|
||||
Label::new(
|
||||
"You can also assign the ANTHROPIC_API_KEY environment variable and restart Zed.",
|
||||
)
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue