diff --git a/crates/agent/src/message_editor.rs b/crates/agent/src/message_editor.rs index 6544eee4f6..824f58d888 100644 --- a/crates/agent/src/message_editor.rs +++ b/crates/agent/src/message_editor.rs @@ -39,7 +39,7 @@ use proto::Plan; use settings::Settings; use std::time::Duration; use theme::ThemeSettings; -use ui::{Disclosure, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*}; +use ui::{Callout, Disclosure, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*}; use util::{ResultExt as _, maybe}; use workspace::{CollaboratorId, Workspace}; use zed_llm_client::CompletionIntent; @@ -1175,6 +1175,7 @@ impl MessageEditor { .map_or(false, |model| { model.provider.id().0 == ZED_CLOUD_PROVIDER_ID }); + if !is_using_zed_provider { return None; } @@ -1229,14 +1230,6 @@ impl MessageEditor { token_usage_ratio: TokenUsageRatio, cx: &mut Context, ) -> Option
{ - let title = if token_usage_ratio == TokenUsageRatio::Exceeded { - "Thread reached the token limit" - } else { - "Thread reaching the token limit soon" - }; - - let message = "Start a new thread from a summary to continue the conversation."; - let icon = if token_usage_ratio == TokenUsageRatio::Exceeded { Icon::new(IconName::X) .color(Color::Error) @@ -1247,19 +1240,36 @@ impl MessageEditor { .size(IconSize::XSmall) }; + let title = if token_usage_ratio == TokenUsageRatio::Exceeded { + "Thread reached the token limit" + } else { + "Thread reaching the token limit soon" + }; + Some( div() - .child(ui::Callout::multi_line( - title, - message, - icon, - "Start New Thread", - Box::new(cx.listener(|this, _, window, cx| { - let from_thread_id = Some(this.thread.read(cx).id().clone()); - window.dispatch_action(Box::new(NewThread { from_thread_id }), cx); - })), - )) - .line_height(line_height), + .border_t_1() + .border_color(cx.theme().colors().border) + .child( + Callout::new() + .line_height(line_height) + .icon(icon) + .title(title) + .description( + "Start a new thread from a summary to continue the conversation.", + ) + .primary_action( + Button::new("start-new-thread", "Start New Thread") + .label_size(LabelSize::Small) + .on_click(cx.listener(|this, _, window, cx| { + let from_thread_id = Some(this.thread.read(cx).id().clone()); + window.dispatch_action( + Box::new(NewThread { from_thread_id }), + cx, + ); + })), + ), + ), ) } diff --git a/crates/agent/src/ui/preview/usage_callouts.rs b/crates/agent/src/ui/preview/usage_callouts.rs index 8d15829db0..62e2909461 100644 --- a/crates/agent/src/ui/preview/usage_callouts.rs +++ b/crates/agent/src/ui/preview/usage_callouts.rs @@ -2,7 +2,7 @@ use client::zed_urls; use component::{empty_example, example_group_with_title, single_example}; use gpui::{AnyElement, App, IntoElement, RenderOnce, Window}; use language_model::RequestUsage; -use ui::{Callout, Color, Icon, IconName, IconSize, prelude::*}; +use ui::{Callout, prelude::*}; use zed_llm_client::{Plan, UsageLimit}; #[derive(IntoElement, RegisterComponent)] @@ -91,16 +91,23 @@ impl RenderOnce for UsageCallout { .size(IconSize::XSmall) }; - Callout::multi_line( - title, - message, - icon, - button_text, - Box::new(move |_, _, cx| { - cx.open_url(&url); - }), - ) - .into_any_element() + div() + .border_t_1() + .border_color(cx.theme().colors().border) + .child( + Callout::new() + .icon(icon) + .title(title) + .description(message) + .primary_action( + Button::new("upgrade", button_text) + .label_size(LabelSize::Small) + .on_click(move |_, _, cx| { + cx.open_url(&url); + }), + ), + ) + .into_any_element() } } @@ -189,10 +196,8 @@ impl Component for UsageCallout { ); Some( - div() + v_flex() .p_4() - .flex() - .flex_col() .gap_4() .child(free_examples) .child(trial_examples) diff --git a/crates/ui/src/components/callout.rs b/crates/ui/src/components/callout.rs index 9e632ce833..3b9b2f6c2a 100644 --- a/crates/ui/src/components/callout.rs +++ b/crates/ui/src/components/callout.rs @@ -1,51 +1,77 @@ -use gpui::ClickEvent; +use gpui::AnyElement; use crate::prelude::*; +/// A callout component for displaying important information that requires user attention. +/// +/// # Usage Example +/// +/// ``` +/// use ui::{Callout}; +/// +/// Callout::new() +/// .icon(Icon::new(IconName::Warning).color(Color::Warning)) +/// .title(Label::new("Be aware of your subscription!")) +/// .description(Label::new("Your subscription is about to expire. Renew now!")) +/// .primary_action(Button::new("renew", "Renew Now")) +/// .secondary_action(Button::new("remind", "Remind Me Later")) +/// ``` +/// #[derive(IntoElement, RegisterComponent)] pub struct Callout { - title: SharedString, - message: Option, - icon: Icon, - cta_label: SharedString, - cta_action: Box, + icon: Option, + title: Option, + description: Option, + primary_action: Option, + secondary_action: Option, line_height: Option, } impl Callout { - pub fn single_line( - title: impl Into, - icon: Icon, - cta_label: impl Into, - cta_action: Box, - ) -> Self { + /// Creates a new `Callout` component with default styling. + pub fn new() -> Self { Self { - title: title.into(), - message: None, - icon, - cta_label: cta_label.into(), - cta_action, + icon: None, + title: None, + description: None, + primary_action: None, + secondary_action: None, line_height: None, } } - pub fn multi_line( - title: impl Into, - message: impl Into, - icon: Icon, - cta_label: impl Into, - cta_action: Box, - ) -> Self { - Self { - title: title.into(), - message: Some(message.into()), - icon, - cta_label: cta_label.into(), - cta_action, - line_height: None, - } + /// Sets the icon to display in the callout. + pub fn icon(mut self, icon: Icon) -> Self { + self.icon = Some(icon); + self } + /// Sets the title of the callout. + pub fn title(mut self, title: impl Into) -> Self { + self.title = Some(title.into()); + self + } + + /// Sets the description of the callout. + /// The description can be single or multi-line text. + pub fn description(mut self, description: impl Into) -> Self { + self.description = Some(description.into()); + self + } + + /// Sets the primary call-to-action button. + pub fn primary_action(mut self, action: impl IntoElement) -> Self { + self.primary_action = Some(action.into_any_element()); + self + } + + /// Sets an optional secondary call-to-action button. + pub fn secondary_action(mut self, action: impl IntoElement) -> Self { + self.secondary_action = Some(action.into_any_element()); + self + } + + /// Sets a custom line height for the callout content. pub fn line_height(mut self, line_height: Pixels) -> Self { self.line_height = Some(line_height); self @@ -57,57 +83,54 @@ impl RenderOnce for Callout { let line_height = self.line_height.unwrap_or(window.line_height()); h_flex() + .w_full() .p_2() .gap_2() - .w_full() - .items_center() - .justify_between() + .items_start() .bg(cx.theme().colors().panel_background) - .border_t_1() - .border_color(cx.theme().colors().border) .overflow_x_hidden() + .when_some(self.icon, |this, icon| { + this.child(h_flex().h(line_height).justify_center().child(icon)) + }) .child( - h_flex() - .flex_shrink() - .overflow_hidden() - .gap_2() - .items_start() + v_flex() + .w_full() .child( h_flex() .h(line_height) - .items_center() - .justify_center() - .child(self.icon), + .w_full() + .gap_1() + .flex_wrap() + .justify_between() + .when_some(self.title, |this, title| { + this.child(h_flex().child(Label::new(title).size(LabelSize::Small))) + }) + .when( + self.primary_action.is_some() || self.secondary_action.is_some(), + |this| { + this.child( + h_flex() + .gap_1() + .when_some(self.secondary_action, |this, action| { + this.child(action) + }) + .when_some(self.primary_action, |this, action| { + this.child(action) + }), + ) + }, + ), ) - .child( - v_flex() - .flex_shrink() - .overflow_hidden() - .child( - h_flex() - .h(line_height) - .items_center() - .child(Label::new(self.title).size(LabelSize::Small)), - ) - .when_some(self.message, |this, message| { - this.child( - div() - .w_full() - .flex_1() - .child(message) - .text_ui_sm(cx) - .text_color(cx.theme().colors().text_muted), - ) - }), - ), - ) - .child( - div().flex_none().child( - Button::new("cta", self.cta_label) - .on_click(self.cta_action) - .style(ButtonStyle::Filled) - .label_size(LabelSize::Small), - ), + .when_some(self.description, |this, description| { + this.child( + div() + .w_full() + .flex_1() + .child(description) + .text_ui_sm(cx) + .text_color(cx.theme().colors().text_muted), + ) + }), ) } } @@ -126,30 +149,75 @@ impl Component for Callout { fn preview(_window: &mut Window, _cx: &mut App) -> Option { let callout_examples = vec![ single_example( - "Single Line", - Callout::single_line( - "Your settings contain deprecated values, please update them.", - Icon::new(IconName::Warning) - .color(Color::Warning) - .size(IconSize::Small), - "Backup & Update", - Box::new(|_, _, _| {}), - ) - .into_any_element(), + "Simple with Title Only", + Callout::new() + .icon( + Icon::new(IconName::Info) + .color(Color::Accent) + .size(IconSize::Small), + ) + .title("System maintenance scheduled for tonight") + .primary_action(Button::new("got-it", "Got it").label_size(LabelSize::Small)) + .into_any_element(), ) .width(px(580.)), single_example( - "Multi Line", - Callout::multi_line( - "Thread reached the token limit", - "Start a new thread from a summary to continue the conversation.", - Icon::new(IconName::X) - .color(Color::Error) - .size(IconSize::Small), - "Start New Thread", - Box::new(|_, _, _| {}), - ) - .into_any_element(), + "With Title and Description", + Callout::new() + .icon( + Icon::new(IconName::Warning) + .color(Color::Warning) + .size(IconSize::Small), + ) + .title("Your settings contain deprecated values") + .description( + "We'll backup your current settings and update them to the new format.", + ) + .primary_action( + Button::new("update", "Backup & Update").label_size(LabelSize::Small), + ) + .secondary_action( + Button::new("dismiss", "Dismiss").label_size(LabelSize::Small), + ) + .into_any_element(), + ) + .width(px(580.)), + single_example( + "Error with Multiple Actions", + Callout::new() + .icon( + Icon::new(IconName::X) + .color(Color::Error) + .size(IconSize::Small), + ) + .title("Thread reached the token limit") + .description("Start a new thread from a summary to continue the conversation.") + .primary_action( + Button::new("new-thread", "Start New Thread").label_size(LabelSize::Small), + ) + .secondary_action( + Button::new("view-summary", "View Summary").label_size(LabelSize::Small), + ) + .into_any_element(), + ) + .width(px(580.)), + single_example( + "Multi-line Description", + Callout::new() + .icon( + Icon::new(IconName::Sparkle) + .color(Color::Accent) + .size(IconSize::Small), + ) + .title("Upgrade to Pro") + .description("• Unlimited threads\n• Priority support\n• Advanced analytics") + .primary_action( + Button::new("upgrade", "Upgrade Now").label_size(LabelSize::Small), + ) + .secondary_action( + Button::new("learn-more", "Learn More").label_size(LabelSize::Small), + ) + .into_any_element(), ) .width(px(580.)), ];