diff --git a/Cargo.lock b/Cargo.lock index 4e3ded5bb6..e8b2dfadd4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -109,6 +109,7 @@ dependencies = [ "gpui", "isahc", "language", + "menu", "schemars", "search", "serde", diff --git a/assets/settings/default.json b/assets/settings/default.json index 695061a0aa..91868fb1e5 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -85,7 +85,7 @@ // Where to dock the assistant. Can be 'left', 'right' or 'bottom'. "dock": "right", // Default width when the assistant is docked to the left or right. - "default_width": 480, + "default_width": 450, // Default height when the assistant is docked to the bottom. "default_height": 320, // OpenAI API key. diff --git a/crates/ai/Cargo.toml b/crates/ai/Cargo.toml index ce2a3338eb..9052b1e5ed 100644 --- a/crates/ai/Cargo.toml +++ b/crates/ai/Cargo.toml @@ -15,6 +15,7 @@ editor = { path = "../editor" } fs = { path = "../fs" } gpui = { path = "../gpui" } language = { path = "../language" } +menu = { path = "../menu" } search = { path = "../search" } settings = { path = "../settings" } theme = { path = "../theme" } diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index ecc27538ed..1bee432b7b 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -40,6 +40,7 @@ pub fn init(cx: &mut AppContext) { cx.add_action(AssistantEditor::assist); cx.capture_action(AssistantEditor::cancel_last_assist); cx.add_action(AssistantEditor::quote_selection); + cx.add_action(AssistantPanel::save_api_key); } pub enum AssistantPanelEvent { @@ -54,6 +55,7 @@ pub struct AssistantPanel { width: Option, height: Option, pane: ViewHandle, + api_key_editor: ViewHandle, languages: Arc, fs: Arc, _subscriptions: Vec, @@ -124,6 +126,17 @@ impl AssistantPanel { }); let mut this = Self { pane, + api_key_editor: cx.add_view(|cx| { + let mut editor = Editor::single_line( + Some(Arc::new(|theme| theme.assistant.api_key_editor.clone())), + cx, + ); + editor.set_placeholder_text( + "sk-000000000000000000000000000000000000000000000000", + cx, + ); + editor + }), languages: workspace.app_state().languages.clone(), fs: workspace.app_state().fs.clone(), width: None, @@ -150,6 +163,9 @@ impl AssistantPanel { .clone(); if old_openai_api_key != new_openai_api_key { old_openai_api_key = new_openai_api_key; + if this.has_focus(cx) { + cx.focus_self(); + } cx.notify(); } }), @@ -183,6 +199,17 @@ impl AssistantPanel { pane.add_item(Box::new(editor), true, focus, None, cx) }); } + + fn save_api_key(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { + let api_key = self.api_key_editor.read(cx).text(cx); + if !api_key.is_empty() { + settings::update_settings_file::( + self.fs.clone(), + cx, + move |settings| settings.openai_api_key = Some(api_key), + ); + } + } } impl Entity for AssistantPanel { @@ -195,12 +222,44 @@ impl View for AssistantPanel { } fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - ChildView::new(&self.pane, cx).into_any() + let style = &theme::current(cx).assistant; + if settings::get::(cx) + .openai_api_key + .is_none() + { + Flex::column() + .with_child( + Text::new( + "Paste your OpenAI API key and press Enter to use the assistant", + style.api_key_prompt.text.clone(), + ) + .aligned(), + ) + .with_child( + ChildView::new(&self.api_key_editor, cx) + .contained() + .with_style(style.api_key_editor.container) + .aligned(), + ) + .contained() + .with_style(style.api_key_prompt.container) + .aligned() + .into_any() + } else { + ChildView::new(&self.pane, cx).into_any() + } } fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { if cx.is_self_focused() { - cx.focus(&self.pane); + if settings::get::(cx) + .openai_api_key + .is_some() + { + cx.focus(&self.pane); + } else { + cx.focus(&self.api_key_editor); + } } } } @@ -290,7 +349,7 @@ impl Panel for AssistantPanel { } fn has_focus(&self, cx: &WindowContext) -> bool { - self.pane.read(cx).has_focus() + self.pane.read(cx).has_focus() || self.api_key_editor.is_focused(cx) } fn is_focus_event(event: &Self::Event) -> bool { diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index f1e752b763..8282336ba5 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -976,6 +976,8 @@ pub struct AssistantStyle { pub sent_at: ContainedText, pub user_sender: ContainedText, pub assistant_sender: ContainedText, + pub api_key_editor: FieldEditor, + pub api_key_prompt: ContainedText, } #[derive(Clone, Deserialize, Default)] diff --git a/styles/src/styleTree/assistant.ts b/styles/src/styleTree/assistant.ts index 0ff65a22ae..085e43071c 100644 --- a/styles/src/styleTree/assistant.ts +++ b/styles/src/styleTree/assistant.ts @@ -1,5 +1,5 @@ import { ColorScheme } from "../themes/common/colorScheme" -import { text, border } from "./components" +import { text, border, background } from "./components" import editor from "./editor" export default function assistant(colorScheme: ColorScheme) { @@ -22,6 +22,26 @@ export default function assistant(colorScheme: ColorScheme) { sent_at: { margin: { top: 2, left: 8 }, ...text(layer, "sans", "default", { size: "2xs" }), + }, + apiKeyEditor: { + background: background(layer, "on"), + cornerRadius: 6, + text: text(layer, "mono", "on"), + placeholderText: text(layer, "mono", "on", "disabled", { + size: "xs", + }), + selection: colorScheme.players[0], + border: border(layer, "on"), + padding: { + bottom: 4, + left: 8, + right: 8, + top: 4, + }, + }, + apiKeyPrompt: { + padding: 10, + ...text(layer, "sans", "default", { size: "xs" }), } } }