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
|
@ -9,7 +9,7 @@ pub mod settings;
|
|||
use anyhow::Result;
|
||||
use client::Client;
|
||||
use futures::{future::BoxFuture, stream::BoxStream};
|
||||
use gpui::{AnyView, AppContext, AsyncAppContext, SharedString, Task, WindowContext};
|
||||
use gpui::{AnyView, AppContext, AsyncAppContext, FocusHandle, SharedString, Task, WindowContext};
|
||||
pub use model::*;
|
||||
use project::Fs;
|
||||
pub(crate) use rate_limiter::*;
|
||||
|
@ -84,7 +84,7 @@ pub trait LanguageModelProvider: 'static {
|
|||
fn load_model(&self, _model: Arc<dyn LanguageModel>, _cx: &AppContext) {}
|
||||
fn is_authenticated(&self, cx: &AppContext) -> bool;
|
||||
fn authenticate(&self, cx: &mut AppContext) -> Task<Result<()>>;
|
||||
fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView;
|
||||
fn configuration_view(&self, cx: &mut WindowContext) -> (AnyView, Option<FocusHandle>);
|
||||
fn reset_credentials(&self, cx: &mut AppContext) -> Task<Result<()>>;
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ use anyhow::{anyhow, Context as _, Result};
|
|||
use client::Client;
|
||||
use collections::BTreeMap;
|
||||
use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
|
||||
use gpui::{AnyView, AppContext, AsyncAppContext, ModelContext, Subscription, Task};
|
||||
use gpui::{AnyView, AppContext, AsyncAppContext, FocusHandle, ModelContext, Subscription, Task};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsStore};
|
||||
|
@ -21,7 +21,7 @@ use crate::LanguageModelProvider;
|
|||
use super::anthropic::count_anthropic_tokens;
|
||||
|
||||
pub const PROVIDER_ID: &str = "zed.dev";
|
||||
pub const PROVIDER_NAME: &str = "zed.dev";
|
||||
pub const PROVIDER_NAME: &str = "Zed AI";
|
||||
|
||||
#[derive(Default, Clone, Debug, PartialEq)]
|
||||
pub struct ZedDotDevSettings {
|
||||
|
@ -57,6 +57,10 @@ pub struct State {
|
|||
}
|
||||
|
||||
impl State {
|
||||
fn is_connected(&self) -> bool {
|
||||
self.status.is_connected()
|
||||
}
|
||||
|
||||
fn authenticate(&self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
let client = self.client.clone();
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
|
@ -179,15 +183,17 @@ impl LanguageModelProvider for CloudLanguageModelProvider {
|
|||
self.state.read(cx).status.is_connected()
|
||||
}
|
||||
|
||||
fn authenticate(&self, cx: &mut AppContext) -> Task<Result<()>> {
|
||||
self.state.update(cx, |state, cx| state.authenticate(cx))
|
||||
fn authenticate(&self, _cx: &mut AppContext) -> Task<Result<()>> {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView {
|
||||
cx.new_view(|_cx| AuthenticationPrompt {
|
||||
state: self.state.clone(),
|
||||
})
|
||||
.into()
|
||||
fn configuration_view(&self, cx: &mut WindowContext) -> (AnyView, Option<FocusHandle>) {
|
||||
let view = cx
|
||||
.new_view(|_cx| ConfigurationView {
|
||||
state: self.state.clone(),
|
||||
})
|
||||
.into();
|
||||
(view, None)
|
||||
}
|
||||
|
||||
fn reset_credentials(&self, _cx: &mut AppContext) -> Task<Result<()>> {
|
||||
|
@ -376,38 +382,88 @@ impl LanguageModel for CloudLanguageModel {
|
|||
}
|
||||
}
|
||||
|
||||
struct AuthenticationPrompt {
|
||||
struct ConfigurationView {
|
||||
state: gpui::Model<State>,
|
||||
}
|
||||
|
||||
impl Render for AuthenticationPrompt {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
const LABEL: &str = "Generate and analyze code with language models. You can dialog with the assistant in this panel or transform code inline.";
|
||||
|
||||
v_flex().gap_6().p_4().child(Label::new(LABEL)).child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Button::new("sign_in", "Sign in")
|
||||
.icon_color(Color::Muted)
|
||||
.icon(IconName::Github)
|
||||
.icon_position(IconPosition::Start)
|
||||
.style(ButtonStyle::Filled)
|
||||
.full_width()
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.state.update(cx, |provider, cx| {
|
||||
provider.authenticate(cx).detach_and_log_err(cx);
|
||||
cx.notify();
|
||||
});
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
div().flex().w_full().items_center().child(
|
||||
Label::new("Sign in to enable collaboration.")
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small),
|
||||
),
|
||||
),
|
||||
)
|
||||
impl ConfigurationView {
|
||||
fn authenticate(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.state.update(cx, |state, cx| {
|
||||
state.authenticate(cx).detach_and_log_err(cx);
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ConfigurationView {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
const ZED_AI_URL: &str = "https://zed.dev/ai";
|
||||
const ACCOUNT_SETTINGS_URL: &str = "https://zed.dev/settings";
|
||||
|
||||
let is_connected = self.state.read(cx).is_connected();
|
||||
|
||||
let is_pro = false;
|
||||
|
||||
if is_connected {
|
||||
v_flex()
|
||||
.gap_3()
|
||||
.max_w_4_5()
|
||||
.child(Label::new(
|
||||
if is_pro {
|
||||
"You have full access to Zed's hosted models from Anthropic, OpenAI, Google through Zed Pro."
|
||||
} else {
|
||||
"You have basic access to models from Anthropic, OpenAI, Google and more through the Zed AI Free plan."
|
||||
}))
|
||||
.child(
|
||||
if is_pro {
|
||||
h_flex().child(
|
||||
Button::new("manage_settings", "Manage Subscription")
|
||||
.style(ButtonStyle::Filled)
|
||||
.on_click(cx.listener(|_, _, cx| {
|
||||
cx.open_url(ACCOUNT_SETTINGS_URL)
|
||||
})))
|
||||
} else {
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Button::new("learn_more", "Learn more")
|
||||
.style(ButtonStyle::Subtle)
|
||||
.on_click(cx.listener(|_, _, cx| {
|
||||
cx.open_url(ZED_AI_URL)
|
||||
})))
|
||||
.child(
|
||||
Button::new("upgrade", "Upgrade")
|
||||
.style(ButtonStyle::Subtle)
|
||||
.color(Color::Accent)
|
||||
.on_click(cx.listener(|_, _, cx| {
|
||||
cx.open_url(ACCOUNT_SETTINGS_URL)
|
||||
})))
|
||||
},
|
||||
)
|
||||
} else {
|
||||
v_flex()
|
||||
.gap_6()
|
||||
.child(Label::new("Use the zed.dev to access language models."))
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Button::new("sign_in", "Sign in")
|
||||
.icon_color(Color::Muted)
|
||||
.icon(IconName::Github)
|
||||
.icon_position(IconPosition::Start)
|
||||
.style(ButtonStyle::Filled)
|
||||
.full_width()
|
||||
.on_click(cx.listener(move |this, _, cx| this.authenticate(cx))),
|
||||
)
|
||||
.child(
|
||||
div().flex().w_full().items_center().child(
|
||||
Label::new("Sign in to enable collaboration.")
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,16 +11,16 @@ use futures::future::BoxFuture;
|
|||
use futures::stream::BoxStream;
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use gpui::{
|
||||
percentage, svg, Animation, AnimationExt, AnyView, AppContext, AsyncAppContext, Model, Render,
|
||||
Subscription, Task, Transformation,
|
||||
percentage, svg, Animation, AnimationExt, AnyView, AppContext, AsyncAppContext, FocusHandle,
|
||||
Model, Render, Subscription, Task, Transformation,
|
||||
};
|
||||
use settings::{Settings, SettingsStore};
|
||||
use std::time::Duration;
|
||||
use strum::IntoEnumIterator;
|
||||
use ui::{
|
||||
div, v_flex, Button, ButtonCommon, Clickable, Color, Context, FixedWidth, IconName,
|
||||
IconPosition, IconSize, IntoElement, Label, LabelCommon, ParentElement, Styled, ViewContext,
|
||||
VisualContext, WindowContext,
|
||||
div, h_flex, v_flex, Button, ButtonCommon, Clickable, Color, Context, FixedWidth, IconName,
|
||||
IconPosition, IconSize, Indicator, IntoElement, Label, LabelCommon, ParentElement, Styled,
|
||||
ViewContext, VisualContext, WindowContext,
|
||||
};
|
||||
|
||||
use crate::settings::AllLanguageModelSettings;
|
||||
|
@ -49,6 +49,14 @@ pub struct State {
|
|||
_settings_subscription: Subscription,
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn is_authenticated(&self, cx: &AppContext) -> bool {
|
||||
CopilotChat::global(cx)
|
||||
.map(|m| m.read(cx).is_authenticated())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
impl CopilotChatLanguageModelProvider {
|
||||
pub fn new(cx: &mut AppContext) -> Self {
|
||||
let state = cx.new_model(|cx| {
|
||||
|
@ -95,9 +103,7 @@ impl LanguageModelProvider for CopilotChatLanguageModelProvider {
|
|||
}
|
||||
|
||||
fn is_authenticated(&self, cx: &AppContext) -> bool {
|
||||
CopilotChat::global(cx)
|
||||
.map(|m| m.read(cx).is_authenticated())
|
||||
.unwrap_or(false)
|
||||
self.state.read(cx).is_authenticated(cx)
|
||||
}
|
||||
|
||||
fn authenticate(&self, cx: &mut AppContext) -> Task<Result<()>> {
|
||||
|
@ -122,29 +128,16 @@ impl LanguageModelProvider for CopilotChatLanguageModelProvider {
|
|||
Task::ready(result)
|
||||
}
|
||||
|
||||
fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView {
|
||||
cx.new_view(|cx| AuthenticationPrompt::new(cx)).into()
|
||||
fn configuration_view(&self, cx: &mut WindowContext) -> (AnyView, Option<FocusHandle>) {
|
||||
let state = self.state.clone();
|
||||
let view = cx.new_view(|cx| ConfigurationView::new(state, cx)).into();
|
||||
(view, None)
|
||||
}
|
||||
|
||||
fn reset_credentials(&self, cx: &mut AppContext) -> Task<Result<()>> {
|
||||
let Some(copilot) = Copilot::global(cx) else {
|
||||
return Task::ready(Err(anyhow::anyhow!(
|
||||
"Copilot is not available. Please ensure Copilot is enabled and running and try again."
|
||||
)));
|
||||
};
|
||||
|
||||
let state = self.state.clone();
|
||||
|
||||
cx.spawn(|mut cx| async move {
|
||||
cx.update_model(&copilot, |model, cx| model.sign_out(cx))?
|
||||
.await?;
|
||||
|
||||
cx.update_model(&state, |_, cx| {
|
||||
cx.notify();
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
fn reset_credentials(&self, _cx: &mut AppContext) -> Task<Result<()>> {
|
||||
Task::ready(Err(anyhow!(
|
||||
"Signing out of GitHub Copilot Chat is currently not supported."
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -281,17 +274,19 @@ impl CopilotChatLanguageModel {
|
|||
}
|
||||
}
|
||||
|
||||
struct AuthenticationPrompt {
|
||||
struct ConfigurationView {
|
||||
copilot_status: Option<copilot::Status>,
|
||||
state: Model<State>,
|
||||
_subscription: Option<Subscription>,
|
||||
}
|
||||
|
||||
impl AuthenticationPrompt {
|
||||
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||
impl ConfigurationView {
|
||||
pub fn new(state: Model<State>, cx: &mut ViewContext<Self>) -> Self {
|
||||
let copilot = Copilot::global(cx);
|
||||
|
||||
Self {
|
||||
copilot_status: copilot.as_ref().map(|copilot| copilot.read(cx).status()),
|
||||
state,
|
||||
_subscription: copilot.as_ref().map(|copilot| {
|
||||
cx.observe(copilot, |this, model, cx| {
|
||||
this.copilot_status = Some(model.read(cx).status());
|
||||
|
@ -302,81 +297,85 @@ impl AuthenticationPrompt {
|
|||
}
|
||||
}
|
||||
|
||||
impl Render for AuthenticationPrompt {
|
||||
impl Render for ConfigurationView {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let loading_icon = svg()
|
||||
.size_8()
|
||||
.path(IconName::ArrowCircle.path())
|
||||
.text_color(cx.text_style().color)
|
||||
.with_animation(
|
||||
"icon_circle_arrow",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|svg, delta| svg.with_transformation(Transformation::rotate(percentage(delta))),
|
||||
);
|
||||
if self.state.read(cx).is_authenticated(cx) {
|
||||
const LABEL: &str = "Authorized.";
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Indicator::dot().color(Color::Success))
|
||||
.child(Label::new(LABEL))
|
||||
} else {
|
||||
let loading_icon = svg()
|
||||
.size_8()
|
||||
.path(IconName::ArrowCircle.path())
|
||||
.text_color(cx.text_style().color)
|
||||
.with_animation(
|
||||
"icon_circle_arrow",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|svg, delta| svg.with_transformation(Transformation::rotate(percentage(delta))),
|
||||
);
|
||||
|
||||
const ERROR_LABEL: &str = "Copilot Chat requires the Copilot plugin to be available and running. Please ensure Copilot is running and try again, or use a different Assistant provider.";
|
||||
match &self.copilot_status {
|
||||
Some(status) => match status {
|
||||
Status::Disabled => {
|
||||
return v_flex().gap_6().p_4().child(Label::new(ERROR_LABEL));
|
||||
}
|
||||
Status::Starting { task: _ } => {
|
||||
const LABEL: &str = "Starting Copilot...";
|
||||
return v_flex()
|
||||
.gap_6()
|
||||
.p_4()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.child(Label::new(LABEL))
|
||||
.child(loading_icon);
|
||||
}
|
||||
Status::SigningIn { prompt: _ } => {
|
||||
const LABEL: &str = "Signing in to Copilot...";
|
||||
return v_flex()
|
||||
.gap_6()
|
||||
.p_4()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.child(Label::new(LABEL))
|
||||
.child(loading_icon);
|
||||
}
|
||||
Status::Error(_) => {
|
||||
const LABEL: &str = "Copilot had issues starting. Please try restarting it. If the issue persists, try reinstalling Copilot.";
|
||||
return v_flex()
|
||||
.gap_6()
|
||||
.p_4()
|
||||
.child(Label::new(LABEL))
|
||||
.child(svg().size_8().path(IconName::CopilotError.path()));
|
||||
}
|
||||
_ => {
|
||||
const LABEL: &str =
|
||||
"To use the assistant panel or inline assistant, you must login to GitHub Copilot. Your GitHub account must have an active Copilot Chat subscription.";
|
||||
v_flex().gap_6().p_4().child(Label::new(LABEL)).child(
|
||||
const ERROR_LABEL: &str = "Copilot Chat requires the Copilot plugin to be available and running. Please ensure Copilot is running and try again, or use a different Assistant provider.";
|
||||
|
||||
match &self.copilot_status {
|
||||
Some(status) => match status {
|
||||
Status::Disabled => v_flex().gap_6().p_4().child(Label::new(ERROR_LABEL)),
|
||||
Status::Starting { task: _ } => {
|
||||
const LABEL: &str = "Starting Copilot...";
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Button::new("sign_in", "Sign In")
|
||||
.icon_color(Color::Muted)
|
||||
.icon(IconName::Github)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_size(IconSize::Medium)
|
||||
.style(ui::ButtonStyle::Filled)
|
||||
.full_width()
|
||||
.on_click(|_, cx| {
|
||||
inline_completion_button::initiate_sign_in(cx)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div().flex().w_full().items_center().child(
|
||||
Label::new("Sign in to start using Github Copilot Chat.")
|
||||
.color(Color::Muted)
|
||||
.size(ui::LabelSize::Small),
|
||||
.gap_6()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.child(Label::new(LABEL))
|
||||
.child(loading_icon)
|
||||
}
|
||||
Status::SigningIn { prompt: _ } => {
|
||||
const LABEL: &str = "Signing in to Copilot...";
|
||||
v_flex()
|
||||
.gap_6()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.child(Label::new(LABEL))
|
||||
.child(loading_icon)
|
||||
}
|
||||
Status::Error(_) => {
|
||||
const LABEL: &str = "Copilot had issues starting. Please try restarting it. If the issue persists, try reinstalling Copilot.";
|
||||
v_flex()
|
||||
.gap_6()
|
||||
.child(Label::new(LABEL))
|
||||
.child(svg().size_8().path(IconName::CopilotError.path()))
|
||||
}
|
||||
_ => {
|
||||
const LABEL: &str =
|
||||
"To use the assistant panel or inline assistant, you must login to GitHub Copilot. Your GitHub account must have an active Copilot Chat subscription.";
|
||||
v_flex().gap_6().child(Label::new(LABEL)).child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Button::new("sign_in", "Sign In")
|
||||
.icon_color(Color::Muted)
|
||||
.icon(IconName::Github)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_size(IconSize::Medium)
|
||||
.style(ui::ButtonStyle::Filled)
|
||||
.full_width()
|
||||
.on_click(|_, cx| {
|
||||
inline_completion_button::initiate_sign_in(cx)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div().flex().w_full().items_center().child(
|
||||
Label::new("Sign in to start using Github Copilot Chat.")
|
||||
.color(Color::Muted)
|
||||
.size(ui::LabelSize::Small),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
None => v_flex().gap_6().p_4().child(Label::new(ERROR_LABEL)),
|
||||
)
|
||||
}
|
||||
},
|
||||
None => v_flex().gap_6().child(Label::new(ERROR_LABEL)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ use crate::{
|
|||
use anyhow::anyhow;
|
||||
use collections::HashMap;
|
||||
use futures::{channel::mpsc, future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
|
||||
use gpui::{AnyView, AppContext, AsyncAppContext, Task};
|
||||
use gpui::{AnyView, AppContext, AsyncAppContext, FocusHandle, Task};
|
||||
use http_client::Result;
|
||||
use std::{
|
||||
future,
|
||||
|
@ -66,7 +66,7 @@ impl LanguageModelProvider for FakeLanguageModelProvider {
|
|||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn authentication_prompt(&self, _: &mut WindowContext) -> AnyView {
|
||||
fn configuration_view(&self, _: &mut WindowContext) -> (AnyView, Option<FocusHandle>) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
|
|
|
@ -4,8 +4,8 @@ use editor::{Editor, EditorElement, EditorStyle};
|
|||
use futures::{future::BoxFuture, FutureExt, StreamExt};
|
||||
use google_ai::stream_generate_content;
|
||||
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;
|
||||
|
@ -14,7 +14,7 @@ use settings::{Settings, SettingsStore};
|
|||
use std::{future, sync::Arc, time::Duration};
|
||||
use strum::IntoEnumIterator;
|
||||
use theme::ThemeSettings;
|
||||
use ui::prelude::*;
|
||||
use ui::{prelude::*, Indicator};
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::{
|
||||
|
@ -49,6 +49,24 @@ pub struct State {
|
|||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn is_authenticated(&self) -> bool {
|
||||
self.api_key.is_some()
|
||||
}
|
||||
|
||||
fn reset_api_key(&self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
let delete_credentials =
|
||||
cx.delete_credentials(&AllLanguageModelSettings::get_global(cx).google.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();
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl GoogleLanguageModelProvider {
|
||||
pub fn new(http_client: Arc<dyn HttpClient>, cx: &mut AppContext) -> Self {
|
||||
let state = cx.new_model(|cx| State {
|
||||
|
@ -118,7 +136,7 @@ impl LanguageModelProvider for GoogleLanguageModelProvider {
|
|||
}
|
||||
|
||||
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<()>> {
|
||||
|
@ -149,9 +167,11 @@ impl LanguageModelProvider for GoogleLanguageModelProvider {
|
|||
}
|
||||
}
|
||||
|
||||
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<()>> {
|
||||
|
@ -267,15 +287,15 @@ impl LanguageModel for GoogleLanguageModel {
|
|||
}
|
||||
}
|
||||
|
||||
struct AuthenticationPrompt {
|
||||
api_key: View<Editor>,
|
||||
struct ConfigurationView {
|
||||
api_key_editor: View<Editor>,
|
||||
state: gpui::Model<State>,
|
||||
}
|
||||
|
||||
impl AuthenticationPrompt {
|
||||
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("AIzaSy...", cx);
|
||||
editor
|
||||
|
@ -285,7 +305,7 @@ 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;
|
||||
}
|
||||
|
@ -304,6 +324,14 @@ impl AuthenticationPrompt {
|
|||
.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 {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let text_style = TextStyle {
|
||||
|
@ -321,7 +349,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(),
|
||||
|
@ -332,7 +360,13 @@ impl AuthenticationPrompt {
|
|||
}
|
||||
}
|
||||
|
||||
impl Render for AuthenticationPrompt {
|
||||
impl FocusableView for ConfigurationView {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
self.api_key_editor.read(cx).focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ConfigurationView {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
const INSTRUCTIONS: [&str; 4] = [
|
||||
"To use the Google AI assistant, you need to add your Google AI API key.",
|
||||
|
@ -341,38 +375,48 @@ impl Render for AuthenticationPrompt {
|
|||
"Paste your Google AI 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 GOOGLE_AI_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 GOOGLE_AI_API_KEY environment variable and restart Zed.",
|
||||
)
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
use anyhow::{anyhow, Result};
|
||||
use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
|
||||
use gpui::{AnyView, AppContext, AsyncAppContext, ModelContext, Subscription, Task};
|
||||
use gpui::{AnyView, AppContext, AsyncAppContext, FocusHandle, ModelContext, Subscription, Task};
|
||||
use http_client::HttpClient;
|
||||
use ollama::{
|
||||
get_models, preload_model, stream_chat_completion, ChatMessage, ChatOptions, ChatRequest,
|
||||
};
|
||||
use settings::{Settings, SettingsStore};
|
||||
use std::{future, sync::Arc, time::Duration};
|
||||
use ui::{prelude::*, ButtonLike, ElevationIndex};
|
||||
use ui::{prelude::*, ButtonLike, ElevationIndex, Indicator};
|
||||
|
||||
use crate::{
|
||||
settings::AllLanguageModelSettings, LanguageModel, LanguageModelId, LanguageModelName,
|
||||
|
@ -39,6 +39,10 @@ pub struct State {
|
|||
}
|
||||
|
||||
impl State {
|
||||
fn is_authenticated(&self) -> bool {
|
||||
!self.available_models.is_empty()
|
||||
}
|
||||
|
||||
fn fetch_models(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
let settings = &AllLanguageModelSettings::get_global(cx).ollama;
|
||||
let http_client = self.http_client.clone();
|
||||
|
@ -129,7 +133,7 @@ impl LanguageModelProvider for OllamaLanguageModelProvider {
|
|||
}
|
||||
|
||||
fn is_authenticated(&self, cx: &AppContext) -> bool {
|
||||
!self.state.read(cx).available_models.is_empty()
|
||||
self.state.read(cx).is_authenticated()
|
||||
}
|
||||
|
||||
fn authenticate(&self, cx: &mut AppContext) -> Task<Result<()>> {
|
||||
|
@ -140,14 +144,12 @@ impl LanguageModelProvider for OllamaLanguageModelProvider {
|
|||
}
|
||||
}
|
||||
|
||||
fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView {
|
||||
fn configuration_view(&self, cx: &mut WindowContext) -> (AnyView, Option<FocusHandle>) {
|
||||
let state = self.state.clone();
|
||||
let fetch_models = Box::new(move |cx: &mut WindowContext| {
|
||||
state.update(cx, |this, cx| this.fetch_models(cx))
|
||||
});
|
||||
|
||||
cx.new_view(|cx| DownloadOllamaMessage::new(fetch_models, cx))
|
||||
.into()
|
||||
(
|
||||
cx.new_view(|cx| ConfigurationView::new(state, cx)).into(),
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
fn reset_credentials(&self, cx: &mut AppContext) -> Task<Result<()>> {
|
||||
|
@ -287,16 +289,19 @@ impl LanguageModel for OllamaLanguageModel {
|
|||
}
|
||||
}
|
||||
|
||||
struct DownloadOllamaMessage {
|
||||
retry_connection: Box<dyn Fn(&mut WindowContext) -> Task<Result<()>>>,
|
||||
struct ConfigurationView {
|
||||
state: gpui::Model<State>,
|
||||
}
|
||||
|
||||
impl DownloadOllamaMessage {
|
||||
pub fn new(
|
||||
retry_connection: Box<dyn Fn(&mut WindowContext) -> Task<Result<()>>>,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
Self { retry_connection }
|
||||
impl ConfigurationView {
|
||||
pub fn new(state: gpui::Model<State>, _cx: &mut ViewContext<Self>) -> Self {
|
||||
Self { state }
|
||||
}
|
||||
|
||||
fn retry_connection(&self, cx: &mut WindowContext) {
|
||||
self.state
|
||||
.update(cx, |state, cx| state.fetch_models(cx))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn render_download_button(&self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
|
@ -314,15 +319,7 @@ impl DownloadOllamaMessage {
|
|||
.size(ButtonSize::Large)
|
||||
.layer(ElevationIndex::ModalSurface)
|
||||
.child(Label::new("Retry"))
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
let connected = (this.retry_connection)(cx);
|
||||
|
||||
cx.spawn(|_this, _cx| async move {
|
||||
connected.await?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx)
|
||||
}))
|
||||
.on_click(cx.listener(move |this, _, cx| this.retry_connection(cx)))
|
||||
}
|
||||
|
||||
fn render_next_steps(&self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
|
@ -347,10 +344,22 @@ impl DownloadOllamaMessage {
|
|||
}
|
||||
}
|
||||
|
||||
impl Render for DownloadOllamaMessage {
|
||||
impl Render for ConfigurationView {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.p_4()
|
||||
let is_authenticated = self.state.read(cx).is_authenticated();
|
||||
|
||||
if is_authenticated {
|
||||
v_flex()
|
||||
.size_full()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Indicator::dot().color(Color::Success))
|
||||
.child(Label::new("Ollama configured").size(LabelSize::Small)),
|
||||
)
|
||||
.into_any()
|
||||
} else {
|
||||
v_flex()
|
||||
.size_full()
|
||||
.gap_2()
|
||||
.child(Label::new("To use Ollama models via the assistant, Ollama must be running on your machine with at least one model downloaded.").size(LabelSize::Large))
|
||||
|
@ -369,5 +378,6 @@ impl Render for DownloadOllamaMessage {
|
|||
)
|
||||
.child(self.render_next_steps(cx))
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,8 +3,8 @@ use collections::BTreeMap;
|
|||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use futures::{future::BoxFuture, 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 open_ai::stream_completion;
|
||||
|
@ -14,7 +14,7 @@ use settings::{Settings, SettingsStore};
|
|||
use std::{future, sync::Arc, time::Duration};
|
||||
use strum::IntoEnumIterator;
|
||||
use theme::ThemeSettings;
|
||||
use ui::prelude::*;
|
||||
use ui::{prelude::*, Indicator};
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::{
|
||||
|
@ -50,6 +50,24 @@ pub struct State {
|
|||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn is_authenticated(&self) -> bool {
|
||||
self.api_key.is_some()
|
||||
}
|
||||
|
||||
fn reset_api_key(&self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
let settings = &AllLanguageModelSettings::get_global(cx).openai;
|
||||
let delete_credentials = cx.delete_credentials(&settings.api_url);
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
delete_credentials.await.log_err();
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.api_key = None;
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl OpenAiLanguageModelProvider {
|
||||
pub fn new(http_client: Arc<dyn HttpClient>, cx: &mut AppContext) -> Self {
|
||||
let state = cx.new_model(|cx| State {
|
||||
|
@ -119,7 +137,7 @@ impl LanguageModelProvider for OpenAiLanguageModelProvider {
|
|||
}
|
||||
|
||||
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<()>> {
|
||||
|
@ -149,22 +167,14 @@ impl LanguageModelProvider for OpenAiLanguageModelProvider {
|
|||
}
|
||||
}
|
||||
|
||||
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 settings = &AllLanguageModelSettings::get_global(cx).openai;
|
||||
let delete_credentials = cx.delete_credentials(&settings.api_url);
|
||||
let state = self.state.clone();
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -287,15 +297,15 @@ pub fn count_open_ai_tokens(
|
|||
.boxed()
|
||||
}
|
||||
|
||||
struct AuthenticationPrompt {
|
||||
api_key: View<Editor>,
|
||||
struct ConfigurationView {
|
||||
api_key_editor: View<Editor>,
|
||||
state: gpui::Model<State>,
|
||||
}
|
||||
|
||||
impl AuthenticationPrompt {
|
||||
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",
|
||||
|
@ -308,7 +318,7 @@ 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;
|
||||
}
|
||||
|
@ -327,6 +337,14 @@ impl AuthenticationPrompt {
|
|||
.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 {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let text_style = TextStyle {
|
||||
|
@ -344,7 +362,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(),
|
||||
|
@ -355,7 +373,13 @@ impl AuthenticationPrompt {
|
|||
}
|
||||
}
|
||||
|
||||
impl Render for AuthenticationPrompt {
|
||||
impl FocusableView for ConfigurationView {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
self.api_key_editor.read(cx).focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ConfigurationView {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
const INSTRUCTIONS: [&str; 6] = [
|
||||
"To use the assistant panel or inline assistant, you need to add your OpenAI API key.",
|
||||
|
@ -366,38 +390,48 @@ impl Render for AuthenticationPrompt {
|
|||
"Paste your OpenAI 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 OPENAI_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 OPENAI_API_KEY environment variable and restart Zed.",
|
||||
)
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue