Update assistant styles (#2665)

Nate Butler created

Updates the assistant with some style quality of life changes.

## Changes

Restyled the conversation list

<img width="646" alt="CleanShot 2023-07-10 at 10 25 23@2x"
src="https://github.com/zed-industries/zed/assets/1714999/5c9a4f94-11c1-4d28-8aac-4d38141829a9">

Updated the assistant header to be a bit more compact, and use a new tab
bar icon style. The existing tab bar icons will be updated in a later
PR.

<img width="646" alt="CleanShot 2023-07-10 at 10 26 30@2x"
src="https://github.com/zed-industries/zed/assets/1714999/3ef9a053-59fa-4d34-9b76-3bb2701acb33">

Updated the remaining token indicator to have 3 steps:
<img width="662" alt="CleanShot 2023-07-10 at 10 29 51@2x"
src="https://github.com/zed-industries/zed/assets/1714999/13d31545-5b00-427c-b7da-b4dfeac037d6">

Updated role labels, added a hover state to make it more clear these are
interactive
<img width="984" alt="CleanShot 2023-07-10 at 10 32 28@2x"
src="https://github.com/zed-industries/zed/assets/1714999/24748495-dde4-4ee9-98f1-6a082f0c1d4d">


Release Notes:

- Improved the UI of some elements in the Assistant panel.

Change summary

assets/icons/radix/maximize.svg        |   4 
assets/icons/radix/minimize.svg        |   4 
crates/ai/src/assistant.rs             |   2 
crates/theme/src/theme.rs              |   1 
styles/src/component/tab_bar_button.ts |  55 +++++
styles/src/style_tree/assistant.ts     | 287 ++++++++-------------------
styles/src/theme/create_theme.ts       |   9 
7 files changed, 163 insertions(+), 199 deletions(-)

Detailed changes

assets/icons/radix/maximize.svg 🔗

@@ -0,0 +1,4 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M9.5 1.5H13.5M13.5 1.5V5.5M13.5 1.5C12.1332 2.86683 10.3668 4.63317 9 6" stroke="white" stroke-linecap="round"/>
+<path d="M1.5 9.5V13.5M1.5 13.5L6 9M1.5 13.5H5.5" stroke="white" stroke-linecap="round"/>
+</svg>

assets/icons/radix/minimize.svg 🔗

@@ -0,0 +1,4 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M13 6L9 6M9 6L9 2M9 6C10.3668 4.63316 12.1332 2.86683 13.5 1.5" stroke="white" stroke-linecap="round"/>
+<path d="M6 13L6 9M6 9L1.5 13.5M6 9L2 9" stroke="white" stroke-linecap="round"/>
+</svg>

crates/ai/src/assistant.rs 🔗

@@ -2061,6 +2061,8 @@ impl ConversationEditor {
         let remaining_tokens = self.conversation.read(cx).remaining_tokens()?;
         let remaining_tokens_style = if remaining_tokens <= 0 {
             &style.no_remaining_tokens
+        } else if remaining_tokens <= 500 {
+            &style.low_remaining_tokens
         } else {
             &style.remaining_tokens
         };

crates/theme/src/theme.rs 🔗

@@ -1030,6 +1030,7 @@ pub struct AssistantStyle {
     pub system_sender: Interactive<ContainedText>,
     pub model: Interactive<ContainedText>,
     pub remaining_tokens: ContainedText,
+    pub low_remaining_tokens: ContainedText,
     pub no_remaining_tokens: ContainedText,
     pub error_icon: Icon,
     pub api_key_editor: FieldEditor,

styles/src/component/tab_bar_button.ts 🔗

@@ -0,0 +1,55 @@
+import { Theme, StyleSets } from "../common"
+import { interactive } from "../element"
+import { InteractiveState } from "../element/interactive"
+import { background, foreground } from "../style_tree/components"
+
+interface TabBarButtonOptions {
+    icon: string
+    color?: StyleSets
+}
+
+type TabBarButtonProps = TabBarButtonOptions & {
+    state?: Partial<Record<InteractiveState, Partial<TabBarButtonOptions>>>
+}
+
+export function tab_bar_button(theme: Theme, { icon, color = "base" }: TabBarButtonProps) {
+    const button_spacing = 8
+
+    return (
+        interactive({
+            base: {
+                icon: {
+                    color: foreground(theme.middle, color),
+                    asset: icon,
+                    dimensions: {
+                        width: 15,
+                        height: 15,
+                    },
+                },
+                container: {
+                    corner_radius: 4,
+                    padding: {
+                        top: 4, bottom: 4, left: 4, right: 4
+                    },
+                    margin: {
+                        left: button_spacing / 2,
+                        right: button_spacing / 2,
+                    },
+                },
+            },
+            state: {
+                hovered: {
+                    container: {
+                        background: background(theme.middle, color, "hovered"),
+
+                    }
+                },
+                clicked: {
+                    container: {
+                        background: background(theme.middle, color, "pressed"),
+                    }
+                },
+            },
+        })
+    )
+}

styles/src/style_tree/assistant.ts 🔗

@@ -1,233 +1,133 @@
-import { text, border, background, foreground } from "./components"
-import { interactive } from "../element"
-import { useTheme } from "../theme"
+import { text, border, background, foreground, TextStyle } from "./components"
+import { Interactive, interactive } from "../element"
+import { tab_bar_button } from "../component/tab_bar_button"
+import { StyleSets, useTheme } from "../theme"
+
+type RoleCycleButton = TextStyle & {
+    background?: string
+}
+// TODO: Replace these with zed types
+type RemainingTokens = TextStyle & {
+    background: string,
+    margin: { top: number, right: number },
+    padding: {
+        right: number,
+        left: number,
+        top: number,
+        bottom: number,
+    },
+    corner_radius: number,
+}
 
 export default function assistant(): any {
     const theme = useTheme()
 
+    const interactive_role = (color: StyleSets): Interactive<RoleCycleButton> => {
+        return (
+            interactive({
+                base: {
+                    ...text(theme.highest, "sans", color, { size: "sm" }),
+                },
+                state: {
+                    hovered: {
+                        ...text(theme.highest, "sans", color, { size: "sm" }),
+                        background: background(theme.highest, color, "hovered"),
+                    },
+                    clicked: {
+                        ...text(theme.highest, "sans", color, { size: "sm" }),
+                        background: background(theme.highest, color, "pressed"),
+                    }
+                },
+            })
+        )
+    }
+
+    const tokens_remaining = (color: StyleSets): RemainingTokens => {
+        return (
+            {
+                ...text(theme.highest, "mono", color, { size: "xs" }),
+                background: background(theme.highest, "on", "default"),
+                margin: { top: 12, right: 20 },
+                padding: { right: 4, left: 4, top: 1, bottom: 1 },
+                corner_radius: 6,
+            }
+        )
+    }
+
     return {
         container: {
             background: background(theme.highest),
             padding: { left: 12 },
         },
         message_header: {
-            margin: { bottom: 6, top: 6 },
+            margin: { bottom: 4, top: 4 },
             background: background(theme.highest),
         },
-        hamburger_button: interactive({
-            base: {
-                icon: {
-                    color: foreground(theme.highest, "variant"),
-                    asset: "icons/hamburger_15.svg",
-                    dimensions: {
-                        width: 15,
-                        height: 15,
-                    },
-                },
-                container: {
-                    padding: { left: 12, right: 8.5 },
-                },
-            },
-            state: {
-                hovered: {
-                    icon: {
-                        color: foreground(theme.highest, "hovered"),
-                    },
-                },
-            },
+        hamburger_button: tab_bar_button(theme, {
+            icon: "icons/hamburger_15.svg",
         }),
-        split_button: interactive({
-            base: {
-                icon: {
-                    color: foreground(theme.highest, "variant"),
-                    asset: "icons/split_message_15.svg",
-                    dimensions: {
-                        width: 15,
-                        height: 15,
-                    },
-                },
-                container: {
-                    padding: { left: 8.5, right: 8.5 },
-                },
-            },
-            state: {
-                hovered: {
-                    icon: {
-                        color: foreground(theme.highest, "hovered"),
-                    },
-                },
-            },
+
+        split_button: tab_bar_button(theme, {
+            icon: "icons/split_message_15.svg",
         }),
-        quote_button: interactive({
-            base: {
-                icon: {
-                    color: foreground(theme.highest, "variant"),
-                    asset: "icons/quote_15.svg",
-                    dimensions: {
-                        width: 15,
-                        height: 15,
-                    },
-                },
-                container: {
-                    padding: { left: 8.5, right: 8.5 },
-                },
-            },
-            state: {
-                hovered: {
-                    icon: {
-                        color: foreground(theme.highest, "hovered"),
-                    },
-                },
-            },
+        quote_button: tab_bar_button(theme, {
+            icon: "icons/radix/quote.svg",
         }),
-        assist_button: interactive({
-            base: {
-                icon: {
-                    color: foreground(theme.highest, "variant"),
-                    asset: "icons/assist_15.svg",
-                    dimensions: {
-                        width: 15,
-                        height: 15,
-                    },
-                },
-                container: {
-                    padding: { left: 8.5, right: 8.5 },
-                },
-            },
-            state: {
-                hovered: {
-                    icon: {
-                        color: foreground(theme.highest, "hovered"),
-                    },
-                },
-            },
+        assist_button: tab_bar_button(theme, {
+            icon: "icons/radix/magic-wand.svg",
         }),
-        zoom_in_button: interactive({
-            base: {
-                icon: {
-                    color: foreground(theme.highest, "variant"),
-                    asset: "icons/maximize_8.svg",
-                    dimensions: {
-                        width: 12,
-                        height: 12,
-                    },
-                },
-                container: {
-                    padding: { left: 10, right: 10 },
-                },
-            },
-            state: {
-                hovered: {
-                    icon: {
-                        color: foreground(theme.highest, "hovered"),
-                    },
-                },
-            },
+        zoom_in_button: tab_bar_button(theme, {
+            icon: "icons/radix/maximize.svg",
         }),
-        zoom_out_button: interactive({
-            base: {
-                icon: {
-                    color: foreground(theme.highest, "variant"),
-                    asset: "icons/minimize_8.svg",
-                    dimensions: {
-                        width: 12,
-                        height: 12,
-                    },
-                },
-                container: {
-                    padding: { left: 10, right: 10 },
-                },
-            },
-            state: {
-                hovered: {
-                    icon: {
-                        color: foreground(theme.highest, "hovered"),
-                    },
-                },
-            },
+        zoom_out_button: tab_bar_button(theme, {
+            icon: "icons/radix/minimize.svg",
         }),
-        plus_button: interactive({
-            base: {
-                icon: {
-                    color: foreground(theme.highest, "variant"),
-                    asset: "icons/plus_12.svg",
-                    dimensions: {
-                        width: 12,
-                        height: 12,
-                    },
-                },
-                container: {
-                    padding: { left: 10, right: 10 },
-                },
-            },
-            state: {
-                hovered: {
-                    icon: {
-                        color: foreground(theme.highest, "hovered"),
-                    },
-                },
-            },
+        plus_button: tab_bar_button(theme, {
+            icon: "icons/radix/plus.svg",
         }),
         title: {
-            ...text(theme.highest, "sans", "default", { size: "sm" }),
+            ...text(theme.highest, "sans", "default", { size: "xs" }),
         },
         saved_conversation: {
             container: interactive({
                 base: {
-                    background: background(theme.highest, "on"),
+                    background: background(theme.middle),
                     padding: { top: 4, bottom: 4 },
+                    border: border(theme.middle, "default", { top: true, overlay: true }),
                 },
                 state: {
                     hovered: {
-                        background: background(theme.highest, "on", "hovered"),
+                        background: background(theme.middle, "hovered"),
                     },
+                    clicked: {
+                        background: background(theme.middle, "pressed"),
+                    }
                 },
             }),
             saved_at: {
                 margin: { left: 8 },
-                ...text(theme.highest, "sans", "default", { size: "xs" }),
+                ...text(theme.highest, "sans", "variant", { size: "xs" }),
             },
             title: {
-                margin: { left: 16 },
-                ...text(theme.highest, "sans", "default", {
-                    size: "sm",
-                    weight: "bold",
-                }),
-            },
-        },
-        user_sender: {
-            default: {
+                margin: { left: 12 },
                 ...text(theme.highest, "sans", "default", {
                     size: "sm",
                     weight: "bold",
                 }),
             },
         },
-        assistant_sender: {
-            default: {
-                ...text(theme.highest, "sans", "accent", {
-                    size: "sm",
-                    weight: "bold",
-                }),
-            },
-        },
-        system_sender: {
-            default: {
-                ...text(theme.highest, "sans", "variant", {
-                    size: "sm",
-                    weight: "bold",
-                }),
-            },
-        },
+        user_sender: interactive_role("base"),
+        assistant_sender: interactive_role("accent"),
+        system_sender: interactive_role("warning"),
         sent_at: {
             margin: { top: 2, left: 8 },
-            ...text(theme.highest, "sans", "default", { size: "2xs" }),
+            ...text(theme.highest, "sans", "variant", { size: "2xs" }),
         },
         model: interactive({
             base: {
-                background: background(theme.highest, "on"),
-                margin: { left: 12, right: 12, top: 12 },
-                padding: 4,
+                background: background(theme.highest),
+                margin: { left: 12, right: 4, top: 12 },
+                padding: { right: 4, left: 4, top: 1, bottom: 1 },
                 corner_radius: 4,
                 ...text(theme.highest, "sans", "default", { size: "xs" }),
             },
@@ -238,20 +138,9 @@ export default function assistant(): any {
                 },
             },
         }),
-        remaining_tokens: {
-            background: background(theme.highest, "on"),
-            margin: { top: 12, right: 24 },
-            padding: 4,
-            corner_radius: 4,
-            ...text(theme.highest, "sans", "positive", { size: "xs" }),
-        },
-        no_remaining_tokens: {
-            background: background(theme.highest, "on"),
-            margin: { top: 12, right: 24 },
-            padding: 4,
-            corner_radius: 4,
-            ...text(theme.highest, "sans", "negative", { size: "xs" }),
-        },
+        remaining_tokens: tokens_remaining("positive"),
+        low_remaining_tokens: tokens_remaining("warning"),
+        no_remaining_tokens: tokens_remaining("negative"),
         error_icon: {
             margin: { left: 8 },
             color: foreground(theme.highest, "negative"),
@@ -259,7 +148,7 @@ export default function assistant(): any {
         },
         api_key_editor: {
             background: background(theme.highest, "on"),
-            corner_radius: 6,
+            corner_radius: 4,
             text: text(theme.highest, "mono", "on"),
             placeholder_text: text(theme.highest, "mono", "on", "disabled", {
                 size: "xs",

styles/src/theme/create_theme.ts 🔗

@@ -12,8 +12,17 @@ export interface Theme {
     name: string
     is_light: boolean
 
+    /**
+    * App background, other elements that should sit directly on top of the background.
+    */
     lowest: Layer
+    /**
+    * Panels, tabs, other UI surfaces that sit on top of the background.
+    */
     middle: Layer
+    /**
+    * Editors like code buffers, conversation editors, etc.
+    */
     highest: Layer
 
     ramps: RampSet