moved status to icon with additional information in tooltip

KCaverly created

Change summary

crates/ai/src/embedding.rs              |  22 ---
crates/assistant/src/assistant_panel.rs | 166 ++++++++++++++++++++++----
crates/theme/src/theme.rs               |   9 +
styles/src/style_tree/assistant.ts      |  17 ++
4 files changed, 162 insertions(+), 52 deletions(-)

Detailed changes

crates/ai/src/embedding.rs 🔗

@@ -85,25 +85,6 @@ impl Embedding {
     }
 }
 
-// impl FromSql for Embedding {
-//     fn column_result(value: ValueRef) -> FromSqlResult<Self> {
-//         let bytes = value.as_blob()?;
-//         let embedding: Result<Vec<f32>, Box<bincode::ErrorKind>> = bincode::deserialize(bytes);
-//         if embedding.is_err() {
-//             return Err(rusqlite::types::FromSqlError::Other(embedding.unwrap_err()));
-//         }
-//         Ok(Embedding(embedding.unwrap()))
-//     }
-// }
-
-// impl ToSql for Embedding {
-//     fn to_sql(&self) -> rusqlite::Result<ToSqlOutput> {
-//         let bytes = bincode::serialize(&self.0)
-//             .map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))?;
-//         Ok(ToSqlOutput::Owned(rusqlite::types::Value::Blob(bytes)))
-//     }
-// }
-
 #[derive(Clone)]
 pub struct OpenAIEmbeddings {
     pub client: Arc<dyn HttpClient>,
@@ -290,7 +271,7 @@ impl EmbeddingProvider for OpenAIEmbeddings {
 
         let mut request_number = 0;
         let mut rate_limiting = false;
-        let mut request_timeout: u64 = 30;
+        let mut request_timeout: u64 = 15;
         let mut response: Response<AsyncBody>;
         while request_number < MAX_RETRIES {
             response = self
@@ -300,6 +281,7 @@ impl EmbeddingProvider for OpenAIEmbeddings {
                     request_timeout,
                 )
                 .await?;
+
             request_number += 1;
 
             match response.status() {

crates/assistant/src/assistant_panel.rs 🔗

@@ -52,7 +52,7 @@ use std::{
 };
 use theme::{
     components::{action_button::Button, ComponentExt},
-    AssistantStyle,
+    AssistantStyle, Icon,
 };
 use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt};
 use uuid::Uuid;
@@ -2857,10 +2857,7 @@ impl View for InlineAssistant {
             .with_children(if self.retrieve_context {
                 Some(
                     Flex::row()
-                        .with_child(Label::new(
-                            self.retrieve_context_status(cx),
-                            theme.assistant.inline.context_status.text.clone(),
-                        ))
+                        .with_children(self.retrieve_context_status(cx))
                         .flex(1., true)
                         .aligned(),
                 )
@@ -3110,40 +3107,149 @@ impl InlineAssistant {
         anyhow::Ok(())
     }
 
-    fn retrieve_context_status(&self, cx: &mut ViewContext<Self>) -> String {
+    fn retrieve_context_status(
+        &self,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<AnyElement<InlineAssistant>> {
+        enum ContextStatusIcon {}
         let project = self.project.clone();
-        if let Some(semantic_index) = self.semantic_index.clone() {
-            let status = semantic_index.update(cx, |index, cx| index.status(&project));
-            return match status {
-                // This theoretically shouldnt be a valid code path
-                semantic_index::SemanticIndexStatus::NotAuthenticated => {
-                    "Not Authenticated!\nPlease ensure you have an `OPENAI_API_KEY` in your environment variables.".to_string()
-                }
-                semantic_index::SemanticIndexStatus::Indexed => {
-                    "Indexing Complete!".to_string()
-                }
-                semantic_index::SemanticIndexStatus::Indexing { remaining_files, rate_limit_expiry } => {
-
-                    let mut status = format!("Remaining files to index for Context Retrieval: {remaining_files}");
+        if let Some(semantic_index) = SemanticIndex::global(cx) {
+            let status = semantic_index.update(cx, |index, _| index.status(&project));
+            let theme = theme::current(cx);
+            match status {
+                SemanticIndexStatus::NotAuthenticated {} => Some(
+                    Svg::new("icons/error.svg")
+                        .with_color(theme.assistant.error_icon.color)
+                        .constrained()
+                        .with_width(theme.assistant.error_icon.width)
+                        .contained()
+                        .with_style(theme.assistant.error_icon.container)
+                        .with_tooltip::<ContextStatusIcon>(
+                            self.id,
+                            "Not Authenticated. Please ensure you have a valid 'OPENAI_API_KEY' in your environment variables.",
+                            None,
+                            theme.tooltip.clone(),
+                            cx,
+                        )
+                        .aligned()
+                        .into_any(),
+                ),
+                SemanticIndexStatus::NotIndexed {} => Some(
+                    Svg::new("icons/error.svg")
+                        .with_color(theme.assistant.inline.context_status.error_icon.color)
+                        .constrained()
+                        .with_width(theme.assistant.inline.context_status.error_icon.width)
+                        .contained()
+                        .with_style(theme.assistant.inline.context_status.error_icon.container)
+                        .with_tooltip::<ContextStatusIcon>(
+                            self.id,
+                            "Not Indexed",
+                            None,
+                            theme.tooltip.clone(),
+                            cx,
+                        )
+                        .aligned()
+                        .into_any(),
+                ),
+                SemanticIndexStatus::Indexing {
+                    remaining_files,
+                    rate_limit_expiry,
+                } => {
+
+                    let mut status_text = if remaining_files == 0 {
+                        "Indexing...".to_string()
+                    } else {
+                        format!("Remaining files to index: {remaining_files}")
+                    };
 
                     if let Some(rate_limit_expiry) = rate_limit_expiry {
-                        let remaining_seconds =
-                                rate_limit_expiry.duration_since(Instant::now());
-                        if remaining_seconds > Duration::from_secs(0) {
-                            write!(status, " (rate limit resets in {}s)", remaining_seconds.as_secs()).unwrap();
+                        let remaining_seconds = rate_limit_expiry.duration_since(Instant::now());
+                        if remaining_seconds > Duration::from_secs(0) && remaining_files > 0 {
+                            write!(
+                                status_text,
+                                " (rate limit expires in {}s)",
+                                remaining_seconds.as_secs()
+                            )
+                            .unwrap();
                         }
                     }
-                    status
-                }
-                semantic_index::SemanticIndexStatus::NotIndexed => {
-                    "Not Indexed for Context Retrieval".to_string()
+                    Some(
+                        Svg::new("icons/bolt.svg")
+                            .with_color(theme.assistant.inline.context_status.in_progress_icon.color)
+                            .constrained()
+                            .with_width(theme.assistant.inline.context_status.in_progress_icon.width)
+                            .contained()
+                            .with_style(theme.assistant.inline.context_status.in_progress_icon.container)
+                            .with_tooltip::<ContextStatusIcon>(
+                                self.id,
+                                status_text,
+                                None,
+                                theme.tooltip.clone(),
+                                cx,
+                            )
+                            .aligned()
+                            .into_any(),
+                    )
                 }
-            };
+                SemanticIndexStatus::Indexed {} => Some(
+                    Svg::new("icons/circle_check.svg")
+                        .with_color(theme.assistant.inline.context_status.complete_icon.color)
+                        .constrained()
+                        .with_width(theme.assistant.inline.context_status.complete_icon.width)
+                        .contained()
+                        .with_style(theme.assistant.inline.context_status.complete_icon.container)
+                        .with_tooltip::<ContextStatusIcon>(
+                            self.id,
+                            "Indexing Complete",
+                            None,
+                            theme.tooltip.clone(),
+                            cx,
+                        )
+                        .aligned()
+                        .into_any(),
+                ),
+            }
+        } else {
+            None
         }
-
-        "".to_string()
     }
 
+    // fn retrieve_context_status(&self, cx: &mut ViewContext<Self>) -> String {
+    //     let project = self.project.clone();
+    //     if let Some(semantic_index) = self.semantic_index.clone() {
+    //         let status = semantic_index.update(cx, |index, cx| index.status(&project));
+    //         return match status {
+    //             // This theoretically shouldnt be a valid code path
+    //             // As the inline assistant cant be launched without an API key
+    //             // We keep it here for safety
+    //             semantic_index::SemanticIndexStatus::NotAuthenticated => {
+    //                 "Not Authenticated!\nPlease ensure you have an `OPENAI_API_KEY` in your environment variables.".to_string()
+    //             }
+    //             semantic_index::SemanticIndexStatus::Indexed => {
+    //                 "Indexing Complete!".to_string()
+    //             }
+    //             semantic_index::SemanticIndexStatus::Indexing { remaining_files, rate_limit_expiry } => {
+
+    //                 let mut status = format!("Remaining files to index for Context Retrieval: {remaining_files}");
+
+    //                 if let Some(rate_limit_expiry) = rate_limit_expiry {
+    //                     let remaining_seconds =
+    //                             rate_limit_expiry.duration_since(Instant::now());
+    //                     if remaining_seconds > Duration::from_secs(0) {
+    //                         write!(status, " (rate limit resets in {}s)", remaining_seconds.as_secs()).unwrap();
+    //                     }
+    //                 }
+    //                 status
+    //             }
+    //             semantic_index::SemanticIndexStatus::NotIndexed => {
+    //                 "Not Indexed for Context Retrieval".to_string()
+    //             }
+    //         };
+    //     }
+
+    //     "".to_string()
+    // }
+
     fn toggle_include_conversation(
         &mut self,
         _: &ToggleIncludeConversation,

crates/theme/src/theme.rs 🔗

@@ -1191,7 +1191,14 @@ pub struct InlineAssistantStyle {
     pub pending_edit_background: Color,
     pub include_conversation: ToggleIconButtonStyle,
     pub retrieve_context: ToggleIconButtonStyle,
-    pub context_status: ContainedText,
+    pub context_status: ContextStatusStyle,
+}
+
+#[derive(Clone, Deserialize, Default, JsonSchema)]
+pub struct ContextStatusStyle {
+    pub error_icon: Icon,
+    pub in_progress_icon: Icon,
+    pub complete_icon: Icon,
 }
 
 #[derive(Clone, Deserialize, Default, JsonSchema)]

styles/src/style_tree/assistant.ts 🔗

@@ -80,7 +80,21 @@ export default function assistant(): any {
             },
             pending_edit_background: background(theme.highest, "positive"),
             context_status: {
-                ...text(theme.highest, "mono", "disabled", { size: "sm" }),
+                error_icon: {
+                    margin: { left: 8, right: 8 },
+                    color: foreground(theme.highest, "negative"),
+                    width: 12,
+                },
+                in_progress_icon: {
+                    margin: { left: 8, right: 8 },
+                    color: foreground(theme.highest, "warning"),
+                    width: 12,
+                },
+                complete_icon: {
+                    margin: { left: 8, right: 8 },
+                    color: foreground(theme.highest, "positive"),
+                    width: 12,
+                }
             },
             retrieve_context: toggleable({
                 base: interactive({
@@ -94,6 +108,7 @@ export default function assistant(): any {
                         border: {
                             width: 1., color: background(theme.highest, "on")
                         },
+                        margin: { left: 2 },
                         padding: {
                             left: 4,
                             right: 4,