From 49502af4d3a645187b8c2c844c88864ccd7474dc Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 19 Dec 2023 09:56:29 -0500 Subject: [PATCH 01/12] Update conversation item rendering --- crates/assistant2/src/assistant_panel.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index 79ebb6602d77776ee30f905cacba3ce6434e9512..4c1abbe4b8bd6f858878a2a2858e8c78954e2f98 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -1020,10 +1020,18 @@ impl AssistantPanel { this.open_conversation(path.clone(), cx) .detach_and_log_err(cx) })) - .child(Label::new( - conversation.mtime.format("%F %I:%M%p").to_string(), - )) - .child(Label::new(conversation.title.clone())) + .child( + div() + .flex() + .flex_1() + .gap_2() + .child( + Label::new(conversation.mtime.format("%F %I:%M%p").to_string()) + .color(Color::Muted) + .size(LabelSize::Small), + ) + .child(Label::new(conversation.title.clone()).size(LabelSize::Small)), + ) } fn open_conversation(&mut self, path: PathBuf, cx: &mut ViewContext) -> Task> { From 2c402f9b5deab38e331248fe8d8d87dab5ab2926 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 19 Dec 2023 10:03:26 -0500 Subject: [PATCH 02/12] Ensure conversation items fill the container --- crates/assistant2/src/assistant_panel.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index 4c1abbe4b8bd6f858878a2a2858e8c78954e2f98..ec2fa4fba05832cd678a1bf3b4ebaac2c1aa2649 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -1020,10 +1020,11 @@ impl AssistantPanel { this.open_conversation(path.clone(), cx) .detach_and_log_err(cx) })) + .full_width() .child( div() .flex() - .flex_1() + .w_full() .gap_2() .child( Label::new(conversation.mtime.format("%F %I:%M%p").to_string()) From 42a02e4fb6c0b76457b1bcff2117b2ec2a6fd12c Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 19 Dec 2023 10:03:59 -0500 Subject: [PATCH 03/12] Remove red border --- crates/assistant2/src/assistant_panel.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index ec2fa4fba05832cd678a1bf3b4ebaac2c1aa2649..0cac69661a2035a333c2a5e46dca5be034d0ac3d 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -1174,8 +1174,6 @@ impl Render for AssistantPanel { .into_any_element() }), ) - .border() - .border_color(gpui::red()) } } } From 57efaa92cf6bb43330e1be225a151ecd1eab41ae Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 19 Dec 2023 10:30:46 -0500 Subject: [PATCH 04/12] Style assistant header, update icons --- assets/icons/maximize.svg | 5 +- assets/icons/menu.svg | 4 +- assets/icons/minimize.svg | 5 +- assets/icons/quote.svg | 9 +--- assets/icons/snip.svg | 1 + assets/icons/split_message.svg | 1 - crates/assistant2/src/assistant_panel.rs | 58 ++++++++++++++++++------ crates/ui2/src/components/icon.rs | 6 ++- 8 files changed, 54 insertions(+), 35 deletions(-) create mode 100644 assets/icons/snip.svg delete mode 100644 assets/icons/split_message.svg diff --git a/assets/icons/maximize.svg b/assets/icons/maximize.svg index f37f6a2087f968728170539b379206cca7551b0e..b3504b5701e3081f82c4fc0ee2ec89642fb439c4 100644 --- a/assets/icons/maximize.svg +++ b/assets/icons/maximize.svg @@ -1,4 +1 @@ - - - - + diff --git a/assets/icons/menu.svg b/assets/icons/menu.svg index 060caeecbfd58603530f253248a0c369ba329b4e..6598697ff83fbb54e760711e569c26f5fac6776f 100644 --- a/assets/icons/menu.svg +++ b/assets/icons/menu.svg @@ -1,3 +1 @@ - - - + diff --git a/assets/icons/minimize.svg b/assets/icons/minimize.svg index ec78f152e13eda0c887a18b99b585d0c65acc8a8..0451233cc9b5455396d7655f8a9b1bc4791d0fc7 100644 --- a/assets/icons/minimize.svg +++ b/assets/icons/minimize.svg @@ -1,4 +1 @@ - - - - + diff --git a/assets/icons/quote.svg b/assets/icons/quote.svg index 50205479c300e789b302cb1dcd687aaf7f9353f8..b970db14300b1837691d346498e8af989442e721 100644 --- a/assets/icons/quote.svg +++ b/assets/icons/quote.svg @@ -1,8 +1 @@ - - - + diff --git a/assets/icons/snip.svg b/assets/icons/snip.svg new file mode 100644 index 0000000000000000000000000000000000000000..03ae4ce0399fae65f847224aa26695530dbd1a66 --- /dev/null +++ b/assets/icons/snip.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/split_message.svg b/assets/icons/split_message.svg deleted file mode 100644 index 54d9e81224cbf55eca2a4f354f7fcfc8f98b6854..0000000000000000000000000000000000000000 --- a/assets/icons/split_message.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index 0cac69661a2035a333c2a5e46dca5be034d0ac3d..17a020fb9c9ed9a3a667a9126d358a06095c564e 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -54,7 +54,8 @@ use std::{ }; use theme::ThemeSettings; use ui::{ - h_stack, prelude::*, v_stack, Button, ButtonLike, Icon, IconButton, IconElement, Label, Tooltip, + h_stack, prelude::*, v_stack, Button, ButtonLike, Icon, IconButton, IconElement, Label, TabBar, + Tooltip, }; use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt}; use uuid::Uuid; @@ -939,7 +940,7 @@ impl AssistantPanel { this.set_active_editor_index(this.prev_active_editor_index, cx); } })) - .tooltip(|cx| Tooltip::text("History", cx)) + .tooltip(|cx| Tooltip::text("Conversation History", cx)) } fn render_editor_tools(&self, cx: &mut ViewContext) -> Vec { @@ -955,12 +956,13 @@ impl AssistantPanel { } fn render_split_button(cx: &mut ViewContext) -> impl IntoElement { - IconButton::new("split_button", Icon::SplitMessage) + IconButton::new("split_button", Icon::Snip) .on_click(cx.listener(|this, _event, cx| { if let Some(active_editor) = this.active_editor() { active_editor.update(cx, |editor, cx| editor.split(&Default::default(), cx)); } })) + .icon_size(IconSize::Small) .tooltip(|cx| Tooltip::for_action("Split Message", &Split, cx)) } @@ -971,6 +973,7 @@ impl AssistantPanel { active_editor.update(cx, |editor, cx| editor.assist(&Default::default(), cx)); } })) + .icon_size(IconSize::Small) .tooltip(|cx| Tooltip::for_action("Assist", &Assist, cx)) } @@ -985,6 +988,7 @@ impl AssistantPanel { }); } })) + .icon_size(IconSize::Small) .tooltip(|cx| Tooltip::for_action("Quote Seleciton", &QuoteSelection, cx)) } @@ -993,15 +997,19 @@ impl AssistantPanel { .on_click(cx.listener(|this, _event, cx| { this.new_conversation(cx); })) + .icon_size(IconSize::Small) .tooltip(|cx| Tooltip::for_action("New Conversation", &NewConversation, cx)) } fn render_zoom_button(&self, cx: &mut ViewContext) -> impl IntoElement { let zoomed = self.zoomed; - IconButton::new("zoom_button", Icon::MagnifyingGlass) + IconButton::new("zoom_button", Icon::Maximize) .on_click(cx.listener(|this, _event, cx| { this.toggle_zoom(&ToggleZoom, cx); })) + .selected(zoomed) + .selected_icon(Icon::Minimize) + .icon_size(IconSize::Small) .tooltip(move |cx| { Tooltip::for_action(if zoomed { "Zoom Out" } else { "Zoom In" }, &ToggleZoom, cx) }) @@ -1125,16 +1133,40 @@ impl Render for AssistantPanel { .active_editor() .map(|editor| Label::new(editor.read(cx).title(cx))); - let mut header = h_stack() - .child(Self::render_hamburger_button(cx)) - .children(title); + // let mut header = h_stack() + // .p_1() + // .border_b() + // .border_color(cx.theme().colors().border_variant) + // .bg(cx.theme().colors().toolbar_background) + // .child(div().flex_1()); + + let header = TabBar::new("assistant_header") + .start_child( + h_stack() + .gap_1() + .child(Self::render_hamburger_button(cx)) + .children(title), + ) + .end_child(if self.focus_handle.contains_focused(cx) { + h_stack() + .gap_1() + .children(self.render_editor_tools(cx)) + .child(Self::render_plus_button(cx)) + .child(self.render_zoom_button(cx)) + } else { + div() + }); - if self.focus_handle.contains_focused(cx) { - header = header - .children(self.render_editor_tools(cx)) - .child(Self::render_plus_button(cx)) - .child(self.render_zoom_button(cx)); - } + // if self.focus_handle.contains_focused(cx) { + // header = header.child( + // div() + // .flex() + // .gap_1() + // .children(self.render_editor_tools(cx)) + // .child(Self::render_plus_button(cx)) + // .child(self.render_zoom_button(cx)), + // ); + // } v_stack() .size_full() diff --git a/crates/ui2/src/components/icon.rs b/crates/ui2/src/components/icon.rs index 6216acac483489f283366177fe5b6416495122c9..ca50cae7f8936e50dbc66be2def222589a571207 100644 --- a/crates/ui2/src/components/icon.rs +++ b/crates/ui2/src/components/icon.rs @@ -75,6 +75,7 @@ pub enum Icon { MagnifyingGlass, MailOpen, Maximize, + Minimize, Menu, MessageBubbles, Mic, @@ -88,7 +89,7 @@ pub enum Icon { Screen, SelectAll, Split, - SplitMessage, + Snip, Terminal, WholeWord, XCircle, @@ -156,6 +157,7 @@ impl Icon { Icon::MagnifyingGlass => "icons/magnifying_glass.svg", Icon::MailOpen => "icons/mail-open.svg", Icon::Maximize => "icons/maximize.svg", + Icon::Minimize => "icons/minimize.svg", Icon::Menu => "icons/menu.svg", Icon::MessageBubbles => "icons/conversations.svg", Icon::Mic => "icons/mic.svg", @@ -169,7 +171,7 @@ impl Icon { Icon::Screen => "icons/desktop.svg", Icon::SelectAll => "icons/select-all.svg", Icon::Split => "icons/split.svg", - Icon::SplitMessage => "icons/split_message.svg", + Icon::Snip => "icons/snip.svg", Icon::Terminal => "icons/terminal.svg", Icon::WholeWord => "icons/word_search.svg", Icon::XCircle => "icons/error.svg", From 286f654517ad64bd2a342951de14358573ae554e Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 19 Dec 2023 10:57:36 -0500 Subject: [PATCH 05/12] Update assistant header --- crates/assistant2/src/assistant_panel.rs | 53 ++++++++++-------------- crates/ui2/src/components/divider.rs | 28 ++++++++++++- crates/ui2/src/components/tab.rs | 6 +-- 3 files changed, 51 insertions(+), 36 deletions(-) diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index 17a020fb9c9ed9a3a667a9126d358a06095c564e..b0b1378dbd2e3c3f91e8ed5603f49cad1635c778 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -55,7 +55,7 @@ use std::{ use theme::ThemeSettings; use ui::{ h_stack, prelude::*, v_stack, Button, ButtonLike, Icon, IconButton, IconElement, Label, TabBar, - Tooltip, + Tooltip, TAB_HEIGHT_IN_REMS, }; use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt}; use uuid::Uuid; @@ -1129,45 +1129,36 @@ impl Render for AssistantPanel { .border() .border_color(gpui::red()) } else { - let title = self - .active_editor() - .map(|editor| Label::new(editor.read(cx).title(cx))); - - // let mut header = h_stack() - // .p_1() - // .border_b() - // .border_color(cx.theme().colors().border_variant) - // .bg(cx.theme().colors().toolbar_background) - // .child(div().flex_1()); - let header = TabBar::new("assistant_header") .start_child( - h_stack() - .gap_1() - .child(Self::render_hamburger_button(cx)) - .children(title), + h_stack().gap_1().child(Self::render_hamburger_button(cx)), // .children(title), ) + .children(self.active_editor().map(|editor| { + h_stack() + .h(rems(TAB_HEIGHT_IN_REMS)) + .flex_1() + .px_2() + .child(Label::new(editor.read(cx).title(cx)).into_element()) + })) .end_child(if self.focus_handle.contains_focused(cx) { h_stack() - .gap_1() - .children(self.render_editor_tools(cx)) - .child(Self::render_plus_button(cx)) - .child(self.render_zoom_button(cx)) + .gap_2() + .child(h_stack().gap_1().children(self.render_editor_tools(cx))) + .child( + ui::Divider::vertical() + .inset() + .color(ui::DividerColor::Border), + ) + .child( + h_stack() + .gap_1() + .child(Self::render_plus_button(cx)) + .child(self.render_zoom_button(cx)), + ) } else { div() }); - // if self.focus_handle.contains_focused(cx) { - // header = header.child( - // div() - // .flex() - // .gap_1() - // .children(self.render_editor_tools(cx)) - // .child(Self::render_plus_button(cx)) - // .child(self.render_zoom_button(cx)), - // ); - // } - v_stack() .size_full() .on_action(cx.listener(|this, _: &workspace::NewFile, cx| { diff --git a/crates/ui2/src/components/divider.rs b/crates/ui2/src/components/divider.rs index cb48ce00ae24f031807d8522510aa18bc586d638..20744d6c48e94ea80312acd9bf3ef63b7a30496f 100644 --- a/crates/ui2/src/components/divider.rs +++ b/crates/ui2/src/components/divider.rs @@ -1,4 +1,4 @@ -use gpui::{Div, IntoElement}; +use gpui::{Div, Hsla, IntoElement}; use crate::prelude::*; @@ -7,9 +7,26 @@ enum DividerDirection { Vertical, } +#[derive(Default)] +pub enum DividerColor { + Border, + #[default] + BorderVariant, +} + +impl DividerColor { + pub fn hsla(self, cx: &WindowContext) -> Hsla { + match self { + DividerColor::Border => cx.theme().colors().border, + DividerColor::BorderVariant => cx.theme().colors().border_variant, + } + } +} + #[derive(IntoElement)] pub struct Divider { direction: DividerDirection, + color: DividerColor, inset: bool, } @@ -26,7 +43,7 @@ impl RenderOnce for Divider { this.w_px().h_full().when(self.inset, |this| this.my_1p5()) } }) - .bg(cx.theme().colors().border_variant) + .bg(self.color.hsla(cx)) } } @@ -34,6 +51,7 @@ impl Divider { pub fn horizontal() -> Self { Self { direction: DividerDirection::Horizontal, + color: DividerColor::default(), inset: false, } } @@ -41,6 +59,7 @@ impl Divider { pub fn vertical() -> Self { Self { direction: DividerDirection::Vertical, + color: DividerColor::default(), inset: false, } } @@ -49,4 +68,9 @@ impl Divider { self.inset = true; self } + + pub fn color(mut self, color: DividerColor) -> Self { + self.color = color; + self + } } diff --git a/crates/ui2/src/components/tab.rs b/crates/ui2/src/components/tab.rs index 8114a322e300cac465981438bf8ad02d84ca029e..7f20a923299d27caca8f30a7b5b234d8525f98eb 100644 --- a/crates/ui2/src/components/tab.rs +++ b/crates/ui2/src/components/tab.rs @@ -3,6 +3,8 @@ use gpui::{AnyElement, IntoElement, Stateful}; use smallvec::SmallVec; use std::cmp::Ordering; +pub const TAB_HEIGHT_IN_REMS: f32 = 30. / 16.; + /// The position of a [`Tab`] within a list of tabs. #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum TabPosition { @@ -94,8 +96,6 @@ impl RenderOnce for Tab { type Rendered = Stateful
; fn render(self, cx: &mut WindowContext) -> Self::Rendered { - const HEIGHT_IN_REMS: f32 = 30. / 16.; - let (text_color, tab_bg, _tab_hover_bg, _tab_active_bg) = match self.selected { false => ( cx.theme().colors().text_muted, @@ -112,7 +112,7 @@ impl RenderOnce for Tab { }; self.div - .h(rems(HEIGHT_IN_REMS)) + .h(rems(TAB_HEIGHT_IN_REMS)) .bg(tab_bg) .border_color(cx.theme().colors().border) .map(|this| match self.position { From 30b01b9bc0699763f2ea72dcd273dfe4d9da30d8 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 19 Dec 2023 11:03:08 -0500 Subject: [PATCH 06/12] Update imports, tab height const --- crates/assistant2/src/assistant_panel.rs | 9 +++------ crates/ui2/src/components/tab.rs | 6 +++--- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index b0b1378dbd2e3c3f91e8ed5603f49cad1635c778..94be39f924ee6125e1d1a12fea35d271fe8ddc2e 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -53,10 +53,7 @@ use std::{ time::{Duration, Instant}, }; use theme::ThemeSettings; -use ui::{ - h_stack, prelude::*, v_stack, Button, ButtonLike, Icon, IconButton, IconElement, Label, TabBar, - Tooltip, TAB_HEIGHT_IN_REMS, -}; +use ui::{prelude::*, Tab, TabBar, Tooltip}; use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt}; use uuid::Uuid; use workspace::{ @@ -1129,13 +1126,13 @@ impl Render for AssistantPanel { .border() .border_color(gpui::red()) } else { - let header = TabBar::new("assistant_header") + let header = ui::TabBar::new("assistant_header") .start_child( h_stack().gap_1().child(Self::render_hamburger_button(cx)), // .children(title), ) .children(self.active_editor().map(|editor| { h_stack() - .h(rems(TAB_HEIGHT_IN_REMS)) + .h(rems(ui::Tab::HEIGHT_IN_REMS)) .flex_1() .px_2() .child(Label::new(editor.read(cx).title(cx)).into_element()) diff --git a/crates/ui2/src/components/tab.rs b/crates/ui2/src/components/tab.rs index 7f20a923299d27caca8f30a7b5b234d8525f98eb..eddfab811914b721126ccf777c9818498e405569 100644 --- a/crates/ui2/src/components/tab.rs +++ b/crates/ui2/src/components/tab.rs @@ -3,8 +3,6 @@ use gpui::{AnyElement, IntoElement, Stateful}; use smallvec::SmallVec; use std::cmp::Ordering; -pub const TAB_HEIGHT_IN_REMS: f32 = 30. / 16.; - /// The position of a [`Tab`] within a list of tabs. #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum TabPosition { @@ -50,6 +48,8 @@ impl Tab { } } + pub const HEIGHT_IN_REMS: f32 = 30. / 16.; + pub fn position(mut self, position: TabPosition) -> Self { self.position = position; self @@ -112,7 +112,7 @@ impl RenderOnce for Tab { }; self.div - .h(rems(TAB_HEIGHT_IN_REMS)) + .h(rems(Self::HEIGHT_IN_REMS)) .bg(tab_bg) .border_color(cx.theme().colors().border) .map(|this| match self.position { From ae313ff83075474d1eed8de1ac840a2eafd59710 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 19 Dec 2023 11:16:59 -0500 Subject: [PATCH 07/12] Allow format_distance to take a DateTimeType --- crates/ui2/src/utils/format_distance.rs | 65 +++++++++++++++++-------- 1 file changed, 45 insertions(+), 20 deletions(-) diff --git a/crates/ui2/src/utils/format_distance.rs b/crates/ui2/src/utils/format_distance.rs index d157d30bbebfeb28103c9aea3cc6193253f81e93..f13e92bbe901e5538bad8566e6b2e001e8e4ff78 100644 --- a/crates/ui2/src/utils/format_distance.rs +++ b/crates/ui2/src/utils/format_distance.rs @@ -1,4 +1,23 @@ -use chrono::NaiveDateTime; +use chrono::{DateTime, Local, NaiveDateTime}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum DateTimeType { + Naive(NaiveDateTime), + Local(DateTime), +} + +impl DateTimeType { + /// Converts the DateTimeType to a NaiveDateTime. + /// + /// If the DateTimeType is already a NaiveDateTime, it will be returned as is. + /// If the DateTimeType is a DateTime, it will be converted to a NaiveDateTime. + pub fn to_naive(&self) -> NaiveDateTime { + match self { + DateTimeType::Naive(naive) => *naive, + DateTimeType::Local(local) => local.naive_local(), + } + } +} /// Calculates the distance in seconds between two NaiveDateTime objects. /// It returns a signed integer denoting the difference. If `date` is earlier than `base_date`, the returned value will be negative. @@ -108,13 +127,13 @@ fn distance_string(distance: i64, include_seconds: bool, add_suffix: bool) -> St /// ``` /// /// Output: `"There was about 3 years between the first and last crewed moon landings."` -pub fn naive_format_distance( - date: NaiveDateTime, +pub fn format_distance( + date: DateTimeType, base_date: NaiveDateTime, include_seconds: bool, add_suffix: bool, ) -> String { - let distance = distance_in_seconds(date, base_date); + let distance = distance_in_seconds(date.to_naive(), base_date); distance_string(distance, include_seconds, add_suffix) } @@ -142,14 +161,14 @@ pub fn naive_format_distance( /// ``` /// /// Output: `It's been over 54 years since Apollo 11 first landed on the moon.` -pub fn naive_format_distance_from_now( - datetime: NaiveDateTime, +pub fn format_distance_from_now( + datetime: DateTimeType, include_seconds: bool, add_suffix: bool, ) -> String { let now = chrono::offset::Local::now().naive_local(); - naive_format_distance(datetime, now, include_seconds, add_suffix) + format_distance(datetime, now, include_seconds, add_suffix) } #[cfg(test)] @@ -159,38 +178,44 @@ mod tests { #[test] fn test_naive_format_distance() { - let date = - NaiveDateTime::from_timestamp_opt(9600, 0).expect("Invalid NaiveDateTime for date"); - let base_date = - NaiveDateTime::from_timestamp_opt(0, 0).expect("Invalid NaiveDateTime for base_date"); + let date = DateTimeType::Naive( + NaiveDateTime::from_timestamp_opt(9600, 0).expect("Invalid NaiveDateTime for date"), + ); + let base_date = DateTimeType::Naive( + NaiveDateTime::from_timestamp_opt(0, 0).expect("Invalid NaiveDateTime for base_date"), + ); assert_eq!( "about 2 hours", - naive_format_distance(date, base_date, false, false) + format_distance(date, base_date.to_naive(), false, false) ); } #[test] fn test_naive_format_distance_with_suffix() { - let date = - NaiveDateTime::from_timestamp_opt(9600, 0).expect("Invalid NaiveDateTime for date"); - let base_date = - NaiveDateTime::from_timestamp_opt(0, 0).expect("Invalid NaiveDateTime for base_date"); + let date = DateTimeType::Naive( + NaiveDateTime::from_timestamp_opt(9600, 0).expect("Invalid NaiveDateTime for date"), + ); + let base_date = DateTimeType::Naive( + NaiveDateTime::from_timestamp_opt(0, 0).expect("Invalid NaiveDateTime for base_date"), + ); assert_eq!( "about 2 hours from now", - naive_format_distance(date, base_date, false, true) + format_distance(date, base_date.to_naive(), false, true) ); } #[test] fn test_naive_format_distance_from_now() { - let date = NaiveDateTime::parse_from_str("1969-07-20T00:00:00Z", "%Y-%m-%dT%H:%M:%SZ") - .expect("Invalid NaiveDateTime for date"); + let date = DateTimeType::Naive( + NaiveDateTime::parse_from_str("1969-07-20T00:00:00Z", "%Y-%m-%dT%H:%M:%SZ") + .expect("Invalid NaiveDateTime for date"), + ); assert_eq!( "over 54 years ago", - naive_format_distance_from_now(date, false, true) + format_distance_from_now(date, false, true) ); } From 79653d2175ed1ac8a037fb843465012203af0aa0 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 19 Dec 2023 11:19:18 -0500 Subject: [PATCH 08/12] Rename format distance test names --- crates/ui2/src/utils/format_distance.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/ui2/src/utils/format_distance.rs b/crates/ui2/src/utils/format_distance.rs index f13e92bbe901e5538bad8566e6b2e001e8e4ff78..ec9a8ad5d879296467c8fcb22caf4b25ea4bfa7d 100644 --- a/crates/ui2/src/utils/format_distance.rs +++ b/crates/ui2/src/utils/format_distance.rs @@ -177,7 +177,7 @@ mod tests { use chrono::NaiveDateTime; #[test] - fn test_naive_format_distance() { + fn test_format_distance() { let date = DateTimeType::Naive( NaiveDateTime::from_timestamp_opt(9600, 0).expect("Invalid NaiveDateTime for date"), ); @@ -192,7 +192,7 @@ mod tests { } #[test] - fn test_naive_format_distance_with_suffix() { + fn test_format_distance_with_suffix() { let date = DateTimeType::Naive( NaiveDateTime::from_timestamp_opt(9600, 0).expect("Invalid NaiveDateTime for date"), ); @@ -207,7 +207,7 @@ mod tests { } #[test] - fn test_naive_format_distance_from_now() { + fn test_format_distance_from_now() { let date = DateTimeType::Naive( NaiveDateTime::parse_from_str("1969-07-20T00:00:00Z", "%Y-%m-%dT%H:%M:%SZ") .expect("Invalid NaiveDateTime for date"), @@ -220,7 +220,7 @@ mod tests { } #[test] - fn test_naive_format_distance_string() { + fn test_format_distance_string() { assert_eq!(distance_string(3, false, false), "less than a minute"); assert_eq!(distance_string(7, false, false), "less than a minute"); assert_eq!(distance_string(13, false, false), "less than a minute"); @@ -244,7 +244,7 @@ mod tests { } #[test] - fn test_naive_format_distance_string_include_seconds() { + fn test_format_distance_string_include_seconds() { assert_eq!(distance_string(3, true, false), "less than 5 seconds"); assert_eq!(distance_string(7, true, false), "less than 10 seconds"); assert_eq!(distance_string(13, true, false), "less than 20 seconds"); From d8a8feb45c35f910c199c94f2ae96f5ea2ecc39a Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 19 Dec 2023 11:41:14 -0500 Subject: [PATCH 09/12] Add FormatDistance struct, add hide_prefix option --- crates/ui2/src/utils/format_distance.rs | 265 +++++++++++++++++++----- 1 file changed, 217 insertions(+), 48 deletions(-) diff --git a/crates/ui2/src/utils/format_distance.rs b/crates/ui2/src/utils/format_distance.rs index ec9a8ad5d879296467c8fcb22caf4b25ea4bfa7d..1586a6e155b5ddc10391c25254d2e30b1e89a598 100644 --- a/crates/ui2/src/utils/format_distance.rs +++ b/crates/ui2/src/utils/format_distance.rs @@ -19,6 +19,45 @@ impl DateTimeType { } } +pub struct FormatDistance { + date: DateTimeType, + base_date: DateTimeType, + include_seconds: bool, + add_suffix: bool, + hide_prefix: bool, +} + +impl FormatDistance { + pub fn new(date: DateTimeType, base_date: DateTimeType) -> Self { + Self { + date, + base_date, + include_seconds: false, + add_suffix: false, + hide_prefix: false, + } + } + + pub fn from_now(date: DateTimeType) -> Self { + Self::new(date, DateTimeType::Local(Local::now())) + } + + pub fn include_seconds(mut self, include_seconds: bool) -> Self { + self.include_seconds = include_seconds; + self + } + + pub fn add_suffix(mut self, add_suffix: bool) -> Self { + self.add_suffix = add_suffix; + self + } + + pub fn hide_prefix(mut self, hide_prefix: bool) -> Self { + self.hide_prefix = hide_prefix; + self + } +} + /// Calculates the distance in seconds between two NaiveDateTime objects. /// It returns a signed integer denoting the difference. If `date` is earlier than `base_date`, the returned value will be negative. /// @@ -32,7 +71,12 @@ fn distance_in_seconds(date: NaiveDateTime, base_date: NaiveDateTime) -> i64 { } /// Generates a string describing the time distance between two dates in a human-readable way. -fn distance_string(distance: i64, include_seconds: bool, add_suffix: bool) -> String { +fn distance_string( + distance: i64, + include_seconds: bool, + add_suffix: bool, + hide_prefix: bool, +) -> String { let suffix = if distance < 0 { " from now" } else { " ago" }; let distance = distance.abs(); @@ -43,53 +87,128 @@ fn distance_string(distance: i64, include_seconds: bool, add_suffix: bool) -> St let months = distance / 2_592_000; let string = if distance < 5 && include_seconds { - "less than 5 seconds".to_string() + if hide_prefix { + "5 seconds" + } else { + "less than 5 seconds" + } + .to_string() } else if distance < 10 && include_seconds { - "less than 10 seconds".to_string() + if hide_prefix { + "10 seconds" + } else { + "less than 10 seconds" + } + .to_string() } else if distance < 20 && include_seconds { - "less than 20 seconds".to_string() + if hide_prefix { + "20 seconds" + } else { + "less than 20 seconds" + } + .to_string() } else if distance < 40 && include_seconds { - "half a minute".to_string() + if hide_prefix { + "half a minute" + } else { + "half a minute" + } + .to_string() } else if distance < 60 && include_seconds { - "less than a minute".to_string() + if hide_prefix { + "a minute" + } else { + "less than a minute" + } + .to_string() } else if distance < 90 && include_seconds { "1 minute".to_string() } else if distance < 30 { - "less than a minute".to_string() + if hide_prefix { + "a minute" + } else { + "less than a minute" + } + .to_string() } else if distance < 90 { "1 minute".to_string() } else if distance < 2_700 { format!("{} minutes", minutes) } else if distance < 5_400 { - "about 1 hour".to_string() + if hide_prefix { + "1 hour" + } else { + "about 1 hour" + } + .to_string() } else if distance < 86_400 { - format!("about {} hours", hours) + if hide_prefix { + format!("{} hours", hours) + } else { + format!("about {} hours", hours) + } + .to_string() } else if distance < 172_800 { "1 day".to_string() } else if distance < 2_592_000 { format!("{} days", days) } else if distance < 5_184_000 { - "about 1 month".to_string() + if hide_prefix { + "1 month" + } else { + "about 1 month" + } + .to_string() } else if distance < 7_776_000 { - "about 2 months".to_string() + if hide_prefix { + "2 months" + } else { + "about 2 months" + } + .to_string() } else if distance < 31_540_000 { format!("{} months", months) } else if distance < 39_425_000 { - "about 1 year".to_string() + if hide_prefix { + "1 year" + } else { + "about 1 year" + } + .to_string() } else if distance < 55_195_000 { - "over 1 year".to_string() + if hide_prefix { "1 year" } else { "over 1 year" }.to_string() } else if distance < 63_080_000 { - "almost 2 years".to_string() + if hide_prefix { + "2 years" + } else { + "almost 2 years" + } + .to_string() } else { let years = distance / 31_536_000; let remaining_months = (distance % 31_536_000) / 2_592_000; if remaining_months < 3 { - format!("about {} years", years) + if hide_prefix { + format!("{} years", years) + } else { + format!("about {} years", years) + } + .to_string() } else if remaining_months < 9 { - format!("over {} years", years) + if hide_prefix { + format!("{} years", years) + } else { + format!("over {} years", years) + } + .to_string() } else { - format!("almost {} years", years + 1) + if hide_prefix { + format!("{} years", years + 1) + } else { + format!("almost {} years", years + 1) + } + .to_string() } }; @@ -132,10 +251,11 @@ pub fn format_distance( base_date: NaiveDateTime, include_seconds: bool, add_suffix: bool, + hide_prefix: bool, ) -> String { let distance = distance_in_seconds(date.to_naive(), base_date); - distance_string(distance, include_seconds, add_suffix) + distance_string(distance, include_seconds, add_suffix, hide_prefix) } /// Get the time difference between a date and now as relative human readable string. @@ -165,10 +285,11 @@ pub fn format_distance_from_now( datetime: DateTimeType, include_seconds: bool, add_suffix: bool, + hide_prefix: bool, ) -> String { let now = chrono::offset::Local::now().naive_local(); - format_distance(datetime, now, include_seconds, add_suffix) + format_distance(datetime, now, include_seconds, add_suffix, hide_prefix) } #[cfg(test)] @@ -187,7 +308,7 @@ mod tests { assert_eq!( "about 2 hours", - format_distance(date, base_date.to_naive(), false, false) + format_distance(date, base_date.to_naive(), false, false, false) ); } @@ -202,7 +323,7 @@ mod tests { assert_eq!( "about 2 hours from now", - format_distance(date, base_date.to_naive(), false, true) + format_distance(date, base_date.to_naive(), false, true, false) ); } @@ -215,41 +336,89 @@ mod tests { assert_eq!( "over 54 years ago", - format_distance_from_now(date, false, true) + format_distance_from_now(date, false, true, false) ); } #[test] fn test_format_distance_string() { - assert_eq!(distance_string(3, false, false), "less than a minute"); - assert_eq!(distance_string(7, false, false), "less than a minute"); - assert_eq!(distance_string(13, false, false), "less than a minute"); - assert_eq!(distance_string(21, false, false), "less than a minute"); - assert_eq!(distance_string(45, false, false), "1 minute"); - assert_eq!(distance_string(61, false, false), "1 minute"); - assert_eq!(distance_string(1920, false, false), "32 minutes"); - assert_eq!(distance_string(3902, false, false), "about 1 hour"); - assert_eq!(distance_string(18002, false, false), "about 5 hours"); - assert_eq!(distance_string(86470, false, false), "1 day"); - assert_eq!(distance_string(345880, false, false), "4 days"); - assert_eq!(distance_string(2764800, false, false), "about 1 month"); - assert_eq!(distance_string(5184000, false, false), "about 2 months"); - assert_eq!(distance_string(10368000, false, false), "4 months"); - assert_eq!(distance_string(34694000, false, false), "about 1 year"); - assert_eq!(distance_string(47310000, false, false), "over 1 year"); - assert_eq!(distance_string(61503000, false, false), "almost 2 years"); - assert_eq!(distance_string(160854000, false, false), "about 5 years"); - assert_eq!(distance_string(236550000, false, false), "over 7 years"); - assert_eq!(distance_string(249166000, false, false), "almost 8 years"); + assert_eq!( + distance_string(3, false, false, false), + "less than a minute" + ); + assert_eq!( + distance_string(7, false, false, false), + "less than a minute" + ); + assert_eq!( + distance_string(13, false, false, false), + "less than a minute" + ); + assert_eq!( + distance_string(21, false, false, false), + "less than a minute" + ); + assert_eq!(distance_string(45, false, false, false), "1 minute"); + assert_eq!(distance_string(61, false, false, false), "1 minute"); + assert_eq!(distance_string(1920, false, false, false), "32 minutes"); + assert_eq!(distance_string(3902, false, false, false), "about 1 hour"); + assert_eq!(distance_string(18002, false, false, false), "about 5 hours"); + assert_eq!(distance_string(86470, false, false, false), "1 day"); + assert_eq!(distance_string(345880, false, false, false), "4 days"); + assert_eq!( + distance_string(2764800, false, false, false), + "about 1 month" + ); + assert_eq!( + distance_string(5184000, false, false, false), + "about 2 months" + ); + assert_eq!(distance_string(10368000, false, false, false), "4 months"); + assert_eq!( + distance_string(34694000, false, false, false), + "about 1 year" + ); + assert_eq!( + distance_string(47310000, false, false, false), + "over 1 year" + ); + assert_eq!( + distance_string(61503000, false, false, false), + "almost 2 years" + ); + assert_eq!( + distance_string(160854000, false, false, false), + "about 5 years" + ); + assert_eq!( + distance_string(236550000, false, false, false), + "over 7 years" + ); + assert_eq!( + distance_string(249166000, false, false, false), + "almost 8 years" + ); } #[test] fn test_format_distance_string_include_seconds() { - assert_eq!(distance_string(3, true, false), "less than 5 seconds"); - assert_eq!(distance_string(7, true, false), "less than 10 seconds"); - assert_eq!(distance_string(13, true, false), "less than 20 seconds"); - assert_eq!(distance_string(21, true, false), "half a minute"); - assert_eq!(distance_string(45, true, false), "less than a minute"); - assert_eq!(distance_string(61, true, false), "1 minute"); + assert_eq!( + distance_string(3, true, false, false), + "less than 5 seconds" + ); + assert_eq!( + distance_string(7, true, false, false), + "less than 10 seconds" + ); + assert_eq!( + distance_string(13, true, false, false), + "less than 20 seconds" + ); + assert_eq!(distance_string(21, true, false, false), "half a minute"); + assert_eq!( + distance_string(45, true, false, false), + "less than a minute" + ); + assert_eq!(distance_string(61, true, false, false), "1 minute"); } } From a1085184a1432a52b2cec43e8567d47df477b3ec Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 19 Dec 2023 11:57:57 -0500 Subject: [PATCH 10/12] use `to_string` to return the format distance string from FormatDistance --- crates/ui2/src/utils/format_distance.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/ui2/src/utils/format_distance.rs b/crates/ui2/src/utils/format_distance.rs index 1586a6e155b5ddc10391c25254d2e30b1e89a598..b0fce184ce64e23e1173ddcce77f3532fb0dfe5e 100644 --- a/crates/ui2/src/utils/format_distance.rs +++ b/crates/ui2/src/utils/format_distance.rs @@ -42,6 +42,16 @@ impl FormatDistance { Self::new(date, DateTimeType::Local(Local::now())) } + pub fn to_string(self) -> String { + format_distance( + self.date, + self.base_date.to_naive(), + self.include_seconds, + self.add_suffix, + self.hide_prefix, + ) + } + pub fn include_seconds(mut self, include_seconds: bool) -> Self { self.include_seconds = include_seconds; self From d8eea949eff72581f9e154667533443e2368b499 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 19 Dec 2023 11:58:20 -0500 Subject: [PATCH 11/12] Update assistant panel message header, model switcher --- crates/assistant2/src/assistant_panel.rs | 45 +++++++++++++++++------- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index 94be39f924ee6125e1d1a12fea35d271fe8ddc2e..ca9e0ddade9828e47ea75de5e1538b7b412725f6 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -53,7 +53,11 @@ use std::{ time::{Duration, Instant}, }; use theme::ThemeSettings; -use ui::{prelude::*, Tab, TabBar, Tooltip}; +use ui::{ + prelude::*, + utils::{DateTimeType, FormatDistance}, + ButtonLike, Tab, TabBar, Tooltip, +}; use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt}; use uuid::Uuid; use workspace::{ @@ -1126,13 +1130,13 @@ impl Render for AssistantPanel { .border() .border_color(gpui::red()) } else { - let header = ui::TabBar::new("assistant_header") + let header = TabBar::new("assistant_header") .start_child( h_stack().gap_1().child(Self::render_hamburger_button(cx)), // .children(title), ) .children(self.active_editor().map(|editor| { h_stack() - .h(rems(ui::Tab::HEIGHT_IN_REMS)) + .h(rems(Tab::HEIGHT_IN_REMS)) .flex_1() .px_2() .child(Label::new(editor.read(cx).title(cx)).into_element()) @@ -2278,6 +2282,14 @@ impl ConversationEditor { } Role::System => Label::new("System").color(Color::Warning), }) + .tooltip(|cx| { + Tooltip::with_meta( + "Toggle message role", + None, + "Available roles: You (User), Assistant, System", + cx, + ) + }) .on_click({ let conversation = conversation.clone(); move |_, cx| { @@ -2292,10 +2304,23 @@ impl ConversationEditor { h_stack() .id(("message_header", message_id.0)) - .border() - .border_color(gpui::red()) + .h_12() + .gap_1() + .p_1() + .debug_bg_cyan() .child(sender) - .child(Label::new(message.sent_at.format("%I:%M%P").to_string())) + // TODO: Only show this if the message if the message has been sent + .child( + Label::new( + FormatDistance::from_now(DateTimeType::Local( + message.sent_at, + )) + .hide_prefix(true) + .add_suffix(true) + .to_string(), + ) + .color(Color::Muted), + ) .children( if let MessageStatus::Error(error) = message.status.clone() { Some( @@ -2456,6 +2481,7 @@ impl ConversationEditor { "current_model", self.conversation.read(cx).model.short_name(), ) + .style(ButtonStyle::Filled) .tooltip(move |cx| Tooltip::text("Change Model", cx)) .on_click(cx.listener(|this, _, cx| this.cycle_model(cx))) } @@ -2469,12 +2495,7 @@ impl ConversationEditor { } else { Color::Default }; - Some( - div() - .border() - .border_color(gpui::red()) - .child(Label::new(remaining_tokens.to_string()).color(remaining_tokens_color)), - ) + Some(Label::new(remaining_tokens.to_string()).color(remaining_tokens_color)) } } From 5d95e13cc809ba872969d19998c371214ec07fb9 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 19 Dec 2023 12:04:01 -0500 Subject: [PATCH 12/12] Update assistant editor --- crates/assistant2/src/assistant_panel.rs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index ca9e0ddade9828e47ea75de5e1538b7b412725f6..ec67d684da92fc85be4fcb5efecc040c783901ed 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -2304,10 +2304,9 @@ impl ConversationEditor { h_stack() .id(("message_header", message_id.0)) - .h_12() + .h_11() .gap_1() .p_1() - .debug_bg_cyan() .child(sender) // TODO: Only show this if the message if the message has been sent .child( @@ -2507,15 +2506,21 @@ impl Render for ConversationEditor { fn render(&mut self, cx: &mut ViewContext) -> Self::Element { div() .key_context("ConversationEditor") - .size_full() - .relative() .capture_action(cx.listener(ConversationEditor::cancel_last_assist)) .capture_action(cx.listener(ConversationEditor::save)) .capture_action(cx.listener(ConversationEditor::copy)) .capture_action(cx.listener(ConversationEditor::cycle_message_role)) .on_action(cx.listener(ConversationEditor::assist)) .on_action(cx.listener(ConversationEditor::split)) - .child(self.editor.clone()) + .size_full() + .relative() + .child( + div() + .size_full() + .pl_2() + .bg(cx.theme().colors().editor_background) + .child(self.editor.clone()), + ) .child( h_stack() .absolute()