diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index 1398e6e7e1..0d4f110929 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -26,7 +26,9 @@ use prompt_library::{PromptLibrary, open_prompt_library}; use prompt_store::PromptBuilder; use settings::{Settings, update_settings_file}; use time::UtcOffset; -use ui::{ContextMenu, KeyBinding, PopoverMenu, PopoverMenuHandle, Tab, Tooltip, prelude::*}; +use ui::{ + Banner, ContextMenu, KeyBinding, PopoverMenu, PopoverMenuHandle, Tab, Tooltip, prelude::*, +}; use util::ResultExt as _; use workspace::Workspace; use workspace::dock::{DockPosition, Panel, PanelEvent}; @@ -838,6 +840,7 @@ impl AssistantPanel { v_flex() .size_full() .when(recent_history.is_empty(), |this| { + let configuration_error_ref = &configuration_error; this.child( v_flex() .size_full() @@ -852,84 +855,85 @@ impl AssistantPanel { ), ) .when(no_error, |parent| { - parent.child( - h_flex().child( - Label::new("Ask and build anything.") - .color(Color::Muted) - .mb_2p5(), - ), - ) - .child( - Button::new("new-thread", "Start New Thread") - .icon(IconName::Plus) - .icon_position(IconPosition::Start) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .full_width() - .key_binding(KeyBinding::for_action_in( - &NewThread, - &focus_handle, - window, - cx, - )) - .on_click(|_event, window, cx| { - window.dispatch_action(NewThread.boxed_clone(), cx) - }), - ) - .child( - Button::new("context", "Add Context") - .icon(IconName::FileCode) - .icon_position(IconPosition::Start) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .full_width() - .key_binding(KeyBinding::for_action_in( - &ToggleContextPicker, - &focus_handle, - window, - cx, - )) - .on_click(|_event, window, cx| { - window.dispatch_action(ToggleContextPicker.boxed_clone(), cx) - }), - ) - .child( - Button::new("mode", "Switch Model") - .icon(IconName::DatabaseZap) - .icon_position(IconPosition::Start) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .full_width() - .key_binding(KeyBinding::for_action_in( - &ToggleModelSelector, - &focus_handle, - window, - cx, - )) - .on_click(|_event, window, cx| { - window.dispatch_action(ToggleModelSelector.boxed_clone(), cx) - }), - ) - .child( - Button::new("settings", "View Settings") - .icon(IconName::Settings) - .icon_position(IconPosition::Start) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .full_width() - .key_binding(KeyBinding::for_action_in( - &OpenConfiguration, - &focus_handle, - window, - cx, - )) - .on_click(|_event, window, cx| { - window.dispatch_action(OpenConfiguration.boxed_clone(), cx) - }), - ) + parent + .child( + h_flex().child( + Label::new("Ask and build anything.") + .color(Color::Muted) + .mb_2p5(), + ), + ) + .child( + Button::new("new-thread", "Start New Thread") + .icon(IconName::Plus) + .icon_position(IconPosition::Start) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .full_width() + .key_binding(KeyBinding::for_action_in( + &NewThread, + &focus_handle, + window, + cx, + )) + .on_click(|_event, window, cx| { + window.dispatch_action(NewThread.boxed_clone(), cx) + }), + ) + .child( + Button::new("context", "Add Context") + .icon(IconName::FileCode) + .icon_position(IconPosition::Start) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .full_width() + .key_binding(KeyBinding::for_action_in( + &ToggleContextPicker, + &focus_handle, + window, + cx, + )) + .on_click(|_event, window, cx| { + window.dispatch_action(ToggleContextPicker.boxed_clone(), cx) + }), + ) + .child( + Button::new("mode", "Switch Model") + .icon(IconName::DatabaseZap) + .icon_position(IconPosition::Start) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .full_width() + .key_binding(KeyBinding::for_action_in( + &ToggleModelSelector, + &focus_handle, + window, + cx, + )) + .on_click(|_event, window, cx| { + window.dispatch_action(ToggleModelSelector.boxed_clone(), cx) + }), + ) + .child( + Button::new("settings", "View Settings") + .icon(IconName::Settings) + .icon_position(IconPosition::Start) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .full_width() + .key_binding(KeyBinding::for_action_in( + &OpenConfiguration, + &focus_handle, + window, + cx, + )) + .on_click(|_event, window, cx| { + window.dispatch_action(OpenConfiguration.boxed_clone(), cx) + }), + ) }) .map(|parent| { - match configuration_error { + match configuration_error_ref { Some(ConfigurationError::ProviderNotAuthenticated) | Some(ConfigurationError::NoProvider) => { parent @@ -958,23 +962,27 @@ impl AssistantPanel { }), ) } - Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => parent - .children( + Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => { + parent.children( provider.render_accept_terms( - LanguageModelProviderTosView::ThreadEmptyState, + LanguageModelProviderTosView::ThreadFreshStart, cx, ), - ), + ) + } None => parent, } }) ) }) .when(!recent_history.is_empty(), |parent| { + let focus_handle = focus_handle.clone(); + let configuration_error_ref = &configuration_error; + parent .p_1p5() - .justify_end() - .gap_1() + .justify_end() + .gap_1() .child( h_flex() .pl_1p5() @@ -992,32 +1000,94 @@ impl AssistantPanel { Button::new("view-history", "View All") .style(ButtonStyle::Subtle) .label_size(LabelSize::Small) - .key_binding(KeyBinding::for_action_in( - &OpenHistory, - &self.focus_handle(cx), - window, - cx, - ).map(|kb| kb.size(rems_from_px(12.))),) + .key_binding( + KeyBinding::for_action_in( + &OpenHistory, + &self.focus_handle(cx), + window, + cx, + ).map(|kb| kb.size(rems_from_px(12.))), + ) .on_click(move |_event, window, cx| { window.dispatch_action(OpenHistory.boxed_clone(), cx); }), ), ) - .child(v_flex().gap_1().children( - recent_history.into_iter().map(|entry| { - // TODO: Add keyboard navigation. - match entry { - HistoryEntry::Thread(thread) => { - PastThread::new(thread, cx.entity().downgrade(), false) - .into_any_element() - } - HistoryEntry::Context(context) => { - PastContext::new(context, cx.entity().downgrade(), false) - .into_any_element() - } + .child( + v_flex() + .gap_1() + .children( + recent_history.into_iter().map(|entry| { + // TODO: Add keyboard navigation. + match entry { + HistoryEntry::Thread(thread) => { + PastThread::new(thread, cx.entity().downgrade(), false) + .into_any_element() + } + HistoryEntry::Context(context) => { + PastContext::new(context, cx.entity().downgrade(), false) + .into_any_element() + } + } + }), + ) + ) + .map(|parent| { + match configuration_error_ref { + Some(ConfigurationError::ProviderNotAuthenticated) + | Some(ConfigurationError::NoProvider) => { + parent + .child( + Banner::new() + .severity(ui::Severity::Warning) + .children( + Label::new( + "Configure at least one LLM provider to start using the panel.", + ) + .size(LabelSize::Small), + ) + .action_slot( + Button::new("settings", "Configure Provider") + .style(ButtonStyle::Tinted(ui::TintColor::Warning)) + .label_size(LabelSize::Small) + .key_binding( + KeyBinding::for_action_in( + &OpenConfiguration, + &focus_handle, + window, + cx, + ) + .map(|kb| kb.size(rems_from_px(12.))), + ) + .on_click(|_event, window, cx| { + window.dispatch_action( + OpenConfiguration.boxed_clone(), + cx, + ) + }), + ), + ) } - }), - )) + Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => { + parent + .child( + Banner::new() + .severity(ui::Severity::Warning) + .children( + h_flex() + .w_full() + .children( + provider.render_accept_terms( + LanguageModelProviderTosView::ThreadtEmptyState, + cx, + ), + ), + ), + ) + } + None => parent, + } + }) }) } diff --git a/crates/language_model/src/language_model.rs b/crates/language_model/src/language_model.rs index 3f4b6e1e07..61f1c527da 100644 --- a/crates/language_model/src/language_model.rs +++ b/crates/language_model/src/language_model.rs @@ -353,7 +353,10 @@ pub trait LanguageModelProvider: 'static { #[derive(PartialEq, Eq)] pub enum LanguageModelProviderTosView { - ThreadEmptyState, + /// When there are some past interactions in the Agent Panel. + ThreadtEmptyState, + /// When there are no past interactions in the Agent Panel. + ThreadFreshStart, PromptEditorPopup, Configuration, } diff --git a/crates/language_models/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs index 2d4945a004..5554167f42 100644 --- a/crates/language_models/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -401,56 +401,83 @@ fn render_accept_terms( let accept_terms_disabled = state.read(cx).accept_terms.is_some(); + let thread_fresh_start = matches!(view_kind, LanguageModelProviderTosView::ThreadFreshStart); + let thread_empty_state = matches!(view_kind, LanguageModelProviderTosView::ThreadtEmptyState); + let terms_button = Button::new("terms_of_service", "Terms of Service") .style(ButtonStyle::Subtle) .icon(IconName::ArrowUpRight) .icon_color(Color::Muted) .icon_size(IconSize::XSmall) + .when(thread_empty_state, |this| this.label_size(LabelSize::Small)) .on_click(move |_, _window, cx| cx.open_url("https://zed.dev/terms-of-service")); - let thread_view = match view_kind { - LanguageModelProviderTosView::ThreadEmptyState => true, - LanguageModelProviderTosView::PromptEditorPopup => false, - LanguageModelProviderTosView::Configuration => false, - }; - - let form = v_flex() - .w_full() - .gap_2() - .child( - h_flex() - .flex_wrap() - .when(thread_view, |this| this.justify_center()) - .child(Label::new( - "To start using Zed AI, please read and accept the", - )) - .child(terms_button), - ) - .child({ - let button_container = h_flex().w_full().child( - Button::new("accept_terms", "I accept the Terms of Service") + let button_container = h_flex().child( + Button::new("accept_terms", "I accept the Terms of Service") + .when(!thread_empty_state, |this| { + this.full_width() .style(ButtonStyle::Tinted(TintColor::Accent)) .icon(IconName::Check) .icon_position(IconPosition::Start) .icon_size(IconSize::Small) - .full_width() - .disabled(accept_terms_disabled) - .on_click({ - let state = state.downgrade(); - move |_, _window, cx| { - state - .update(cx, |state, cx| state.accept_terms_of_service(cx)) - .ok(); - } - }), - ); + }) + .when(thread_empty_state, |this| { + this.style(ButtonStyle::Tinted(TintColor::Warning)) + .label_size(LabelSize::Small) + }) + .disabled(accept_terms_disabled) + .on_click({ + let state = state.downgrade(); + move |_, _window, cx| { + state + .update(cx, |state, cx| state.accept_terms_of_service(cx)) + .ok(); + } + }), + ); - match view_kind { - LanguageModelProviderTosView::PromptEditorPopup => button_container.justify_end(), - LanguageModelProviderTosView::Configuration => button_container.justify_start(), - LanguageModelProviderTosView::ThreadEmptyState => button_container.justify_center(), - } - }); + let form = if thread_empty_state { + h_flex() + .w_full() + .flex_wrap() + .justify_between() + .child( + h_flex() + .child( + Label::new("To start using Zed AI, please read and accept the") + .size(LabelSize::Small), + ) + .child(terms_button), + ) + .child(button_container) + } else { + v_flex() + .w_full() + .gap_2() + .child( + h_flex() + .flex_wrap() + .when(thread_fresh_start, |this| this.justify_center()) + .child(Label::new( + "To start using Zed AI, please read and accept the", + )) + .child(terms_button), + ) + .child({ + match view_kind { + LanguageModelProviderTosView::PromptEditorPopup => { + button_container.w_full().justify_end() + } + LanguageModelProviderTosView::Configuration => { + button_container.w_full().justify_start() + } + LanguageModelProviderTosView::ThreadFreshStart => { + button_container.w_full().justify_center() + } + LanguageModelProviderTosView::ThreadtEmptyState => div().w_0(), + } + }) + }; Some(form.into_any()) } diff --git a/crates/ui/src/components/banner.rs b/crates/ui/src/components/banner.rs index 2cf265882a..f8567b3dc1 100644 --- a/crates/ui/src/components/banner.rs +++ b/crates/ui/src/components/banner.rs @@ -130,7 +130,7 @@ impl RenderOnce for Banner { .child(content_area) .child(action_slot); } else { - container = container.px_2().child(content_area); + container = container.px_2().child(div().w_full().child(content_area)); } container