Allow saving the OpenAI API key in the assistant panel

Antonio Scandurra created

Change summary

Cargo.lock                        |  1 
assets/settings/default.json      |  2 
crates/ai/Cargo.toml              |  1 
crates/ai/src/assistant.rs        | 65 +++++++++++++++++++++++++++++++-
crates/theme/src/theme.rs         |  2 +
styles/src/styleTree/assistant.ts | 22 ++++++++++
6 files changed, 88 insertions(+), 5 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -109,6 +109,7 @@ dependencies = [
  "gpui",
  "isahc",
  "language",
+ "menu",
  "schemars",
  "search",
  "serde",

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.

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" }

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<f32>,
     height: Option<f32>,
     pane: ViewHandle<Pane>,
+    api_key_editor: ViewHandle<Editor>,
     languages: Arc<LanguageRegistry>,
     fs: Arc<dyn Fs>,
     _subscriptions: Vec<Subscription>,
@@ -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<Self>) {
+        let api_key = self.api_key_editor.read(cx).text(cx);
+        if !api_key.is_empty() {
+            settings::update_settings_file::<AssistantSettings>(
+                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<Self>) -> AnyElement<Self> {
-        ChildView::new(&self.pane, cx).into_any()
+        let style = &theme::current(cx).assistant;
+        if settings::get::<AssistantSettings>(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<Self>) {
         if cx.is_self_focused() {
-            cx.focus(&self.pane);
+            if settings::get::<AssistantSettings>(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 {

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)]

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" }),
       }
     }
 }