From 43339c6869e9656996ffca79bdfa73f5778555be Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Wed, 5 Mar 2025 21:39:29 -0300 Subject: [PATCH] assistant2: Improve clarity of loading state (#26178) Follow up to https://github.com/zed-industries/zed/pull/23299. Having the loading state on the button makes sense, but it's also too subtle. If you're waiting on an LLM response that takes a while, like a "thinking state", not having anything more clearly visible communicating that the model is still in-progress can make you think something is wrong. Release Notes: - N/A --- crates/assistant2/src/assistant_panel.rs | 7 +- crates/assistant2/src/message_editor.rs | 338 +++++++++++++---------- 2 files changed, 192 insertions(+), 153 deletions(-) diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index ef04205e43..86fad38453 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -1023,12 +1023,7 @@ impl Render for AssistantPanel { .map(|parent| match self.active_view { ActiveView::Thread => parent .child(self.render_active_thread_or_empty_state(window, cx)) - .child( - h_flex() - .border_t_1() - .border_color(cx.theme().colors().border) - .child(self.message_editor.clone()), - ) + .child(h_flex().child(self.message_editor.clone())) .children(self.render_last_error(cx)), ActiveView::History => parent.child(self.history.clone()), ActiveView::PromptEditor => parent.children(self.context_editor.clone()), diff --git a/crates/assistant2/src/message_editor.rs b/crates/assistant2/src/message_editor.rs index e0b35acad1..9dcf411081 100644 --- a/crates/assistant2/src/message_editor.rs +++ b/crates/assistant2/src/message_editor.rs @@ -4,8 +4,8 @@ use editor::actions::MoveUp; use editor::{Editor, EditorElement, EditorEvent, EditorStyle}; use fs::Fs; use gpui::{ - pulsating_between, Animation, AnimationExt, App, DismissEvent, Entity, Focusable, Subscription, - TextStyle, WeakEntity, + Animation, AnimationExt, App, DismissEvent, Entity, Focusable, Subscription, TextStyle, + WeakEntity, }; use language_model::LanguageModelRegistry; use language_model_selector::ToggleModelSelector; @@ -16,7 +16,7 @@ use text::Bias; use theme::ThemeSettings; use ui::{ prelude::*, ButtonLike, KeyBinding, PlatformStyle, PopoverMenu, PopoverMenuHandle, Switch, - TintColor, Tooltip, + Tooltip, }; use vim_mode_setting::VimModeSetting; use workspace::Workspace; @@ -298,166 +298,210 @@ impl Render for MessageEditor { let linux = platform == PlatformStyle::Linux; let windows = platform == PlatformStyle::Windows; let button_width = if linux || windows || vim_mode_enabled { - px(92.) + px(82.) } else { px(64.) }; v_flex() - .key_context("MessageEditor") - .on_action(cx.listener(Self::chat)) - .on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| { - this.model_selector - .update(cx, |model_selector, cx| model_selector.toggle(window, cx)); - })) - .on_action(cx.listener(Self::toggle_context_picker)) - .on_action(cx.listener(Self::remove_all_context)) - .on_action(cx.listener(Self::move_up)) - .on_action(cx.listener(Self::toggle_chat_mode)) .size_full() - .gap_2() - .p_2() - .bg(bg_color) - .child(self.context_strip.clone()) + .when(is_streaming_completion, |parent| { + let focus_handle = self.editor.focus_handle(cx).clone(); + parent.child( + h_flex().py_3().w_full().justify_center().child( + h_flex() + .flex_none() + .pl_2() + .pr_1() + .py_1() + .bg(cx.theme().colors().editor_background) + .border_1() + .border_color(cx.theme().colors().border_variant) + .rounded_lg() + .shadow_md() + .gap_1() + .child( + Icon::new(IconName::ArrowCircle) + .size(IconSize::XSmall) + .color(Color::Muted) + .with_animation( + "arrow-circle", + Animation::new(Duration::from_secs(2)).repeat(), + |icon, delta| { + icon.transform(gpui::Transformation::rotate( + gpui::percentage(delta), + )) + }, + ), + ) + .child( + Label::new("Generating…") + .size(LabelSize::XSmall) + .color(Color::Muted), + ) + .child(ui::Divider::vertical()) + .child( + Button::new("cancel-generation", "Cancel") + .label_size(LabelSize::XSmall) + .key_binding( + KeyBinding::for_action_in( + &editor::actions::Cancel, + &focus_handle, + window, + cx, + ) + .map(|kb| kb.size(rems_from_px(10.))), + ) + .on_click(move |_event, window, cx| { + focus_handle.dispatch_action( + &editor::actions::Cancel, + window, + cx, + ); + }), + ), + ), + ) + }) .child( v_flex() - .gap_5() - .child({ - let settings = ThemeSettings::get_global(cx); - let text_style = TextStyle { - color: cx.theme().colors().text, - font_family: settings.ui_font.family.clone(), - font_features: settings.ui_font.features.clone(), - font_size: font_size.into(), - font_weight: settings.ui_font.weight, - line_height: line_height.into(), - ..Default::default() - }; - - EditorElement::new( - &self.editor, - EditorStyle { - background: bg_color, - local_player: cx.theme().players().local(), - text: text_style, - ..Default::default() - }, - ) - }) + .key_context("MessageEditor") + .on_action(cx.listener(Self::chat)) + .on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| { + this.model_selector + .update(cx, |model_selector, cx| model_selector.toggle(window, cx)); + })) + .on_action(cx.listener(Self::toggle_context_picker)) + .on_action(cx.listener(Self::remove_all_context)) + .on_action(cx.listener(Self::move_up)) + .on_action(cx.listener(Self::toggle_chat_mode)) + .gap_2() + .p_2() + .bg(bg_color) + .border_t_1() + .border_color(cx.theme().colors().border) + .child(self.context_strip.clone()) .child( - PopoverMenu::new("inline-context-picker") - .menu(move |window, cx| { - inline_context_picker.update(cx, |this, cx| { - this.init(window, cx); - }); + v_flex() + .gap_5() + .child({ + let settings = ThemeSettings::get_global(cx); + let text_style = TextStyle { + color: cx.theme().colors().text, + font_family: settings.ui_font.family.clone(), + font_features: settings.ui_font.features.clone(), + font_size: font_size.into(), + font_weight: settings.ui_font.weight, + line_height: line_height.into(), + ..Default::default() + }; - Some(inline_context_picker.clone()) + EditorElement::new( + &self.editor, + EditorStyle { + background: bg_color, + local_player: cx.theme().players().local(), + text: text_style, + ..Default::default() + }, + ) }) - .attach(gpui::Corner::TopLeft) - .anchor(gpui::Corner::BottomLeft) - .offset(gpui::Point { - x: px(0.0), - y: (-ThemeSettings::get_global(cx).ui_font_size(cx) * 2) - px(4.0), - }) - .with_handle(self.inline_context_picker_menu_handle.clone()), - ) - .child( - h_flex() - .justify_between() .child( - Switch::new("use-tools", self.use_tools.into()) - .label("Tools") - .on_click(cx.listener(|this, selection, _window, _cx| { - this.use_tools = match selection { - ToggleState::Selected => true, - ToggleState::Unselected - | ToggleState::Indeterminate => false, - }; - })) - .key_binding(KeyBinding::for_action_in( - &ChatMode, - &focus_handle, - window, - cx, - )), + PopoverMenu::new("inline-context-picker") + .menu(move |window, cx| { + inline_context_picker.update(cx, |this, cx| { + this.init(window, cx); + }); + + Some(inline_context_picker.clone()) + }) + .attach(gpui::Corner::TopLeft) + .anchor(gpui::Corner::BottomLeft) + .offset(gpui::Point { + x: px(0.0), + y: (-ThemeSettings::get_global(cx).ui_font_size(cx) * 2) + - px(4.0), + }) + .with_handle(self.inline_context_picker_menu_handle.clone()), ) - .child(h_flex().gap_1().child(self.model_selector.clone()).child( - if is_streaming_completion { - ButtonLike::new("cancel-generation") - .width(button_width.into()) - .style(ButtonStyle::Tinted(TintColor::Accent)) - .child( - h_flex() - .w_full() - .justify_between() - .child( - Label::new("Cancel") - .size(LabelSize::Small) - .with_animation( - "pulsating-label", - Animation::new(Duration::from_secs(2)) - .repeat() - .with_easing(pulsating_between( - 0.4, 0.8, - )), - |label, delta| label.alpha(delta), - ), - ) - .children( - KeyBinding::for_action_in( - &editor::actions::Cancel, - &focus_handle, - window, - cx, - ) - .map(|binding| binding.into_any_element()), - ), - ) - .on_click(move |_event, window, cx| { - focus_handle.dispatch_action( - &editor::actions::Cancel, + .child( + h_flex() + .justify_between() + .child( + Switch::new("use-tools", self.use_tools.into()) + .label("Tools") + .on_click(cx.listener( + |this, selection, _window, _cx| { + this.use_tools = match selection { + ToggleState::Selected => true, + ToggleState::Unselected + | ToggleState::Indeterminate => false, + }; + }, + )) + .key_binding(KeyBinding::for_action_in( + &ChatMode, + &focus_handle, window, cx, - ); - }) - } else { - ButtonLike::new("submit-message") - .width(button_width.into()) - .style(ButtonStyle::Filled) - .disabled(is_editor_empty || !is_model_selected) - .child( - h_flex() - .w_full() - .justify_between() - .child( - Label::new("Submit") - .size(LabelSize::Small) - .color(submit_label_color), + )), + ) + .child( + h_flex().gap_1().child(self.model_selector.clone()).child( + ButtonLike::new("submit-message") + .width(button_width.into()) + .style(ButtonStyle::Filled) + .disabled( + is_editor_empty + || !is_model_selected + || is_streaming_completion, ) - .children( - KeyBinding::for_action_in( - &Chat, - &focus_handle, - window, - cx, - ) - .map(|binding| binding.into_any_element()), - ), - ) - .on_click(move |_event, window, cx| { - focus_handle.dispatch_action(&Chat, window, cx); - }) - .when(is_editor_empty, |button| { - button - .tooltip(Tooltip::text("Type a message to submit")) - }) - .when(!is_model_selected, |button| { - button.tooltip(Tooltip::text( - "Select a model to continue", - )) - }) - }, - )), + .child( + h_flex() + .w_full() + .justify_between() + .child( + Label::new("Submit") + .size(LabelSize::Small) + .color(submit_label_color), + ) + .children( + KeyBinding::for_action_in( + &Chat, + &focus_handle, + window, + cx, + ) + .map(|binding| { + binding + .when(vim_mode_enabled, |kb| { + kb.size(rems_from_px(12.)) + }) + .into_any_element() + }), + ), + ) + .on_click(move |_event, window, cx| { + focus_handle.dispatch_action(&Chat, window, cx); + }) + .when(is_editor_empty, |button| { + button.tooltip(Tooltip::text( + "Type a message to submit", + )) + }) + .when(is_streaming_completion, |button| { + button.tooltip(Tooltip::text( + "Cancel to submit a new message", + )) + }) + .when(!is_model_selected, |button| { + button.tooltip(Tooltip::text( + "Select a model to continue", + )) + }), + ), + ), + ), ), ) }