Style assistant panel (#3711)

Nate Butler created

[[PR Description]]

Styles most of the assistant panel. A few notes:

- We now cut off the title if it gets to long so the assistant tools
don't get cut off
- I wasn't able to get to the "no api key" state, so that hasn't been
style checked yet.
- A few of icons were updated in this PR

I also added a new tooltip that teaches you a bit about role cycling:

![CleanShot 2023-12-19 at 12 08
15@2x](https://github.com/zed-industries/zed/assets/1714999/89dcdcc0-9ef9-4527-94a2-6146b5f127f2)

🐜 Known issues 🐜

- There is a bug where zooming the panel makes it shift 1px (@maxdeviant
I think this has to do with panel borders)
- We are showing a timestamp for new conversations before you have sent
a message/launched an assist action. I wasn't sure how to case this out.

Before:

![CleanShot 2023-12-19 at 12 06
17@2x](https://github.com/zed-industries/zed/assets/1714999/16154add-a7f4-455a-a5c5-706847e012a2)

![CleanShot 2023-12-19 at 12 05
00@2x](https://github.com/zed-industries/zed/assets/1714999/a6d325ec-59c2-41b3-ad82-6cf88f2f70a0)

After:

![CleanShot 2023-12-19 at 12 05
37@2x](https://github.com/zed-industries/zed/assets/1714999/aaa4668e-b361-411a-8865-1989bd1c3ae1)

![CleanShot 2023-12-19 at 12 05
52@2x](https://github.com/zed-industries/zed/assets/1714999/0c6a9970-56ab-43cc-9394-fd3f6bf0ed6a)


Release Notes:

- N/A

Change summary

assets/icons/maximize.svg                |   5 
assets/icons/menu.svg                    |   4 
assets/icons/minimize.svg                |   5 
assets/icons/quote.svg                   |   4 
assets/icons/snip.svg                    |   1 
assets/icons/split_message.svg           |   0 
crates/assistant2/src/assistant_panel.rs | 125 ++++++--
crates/ui2/src/components/divider.rs     |  28 +
crates/ui2/src/components/icon.rs        |   6 
crates/ui2/src/components/tab.rs         |   6 
crates/ui2/src/utils/format_distance.rs  | 342 ++++++++++++++++++++-----
11 files changed, 399 insertions(+), 127 deletions(-)

Detailed changes

assets/icons/maximize.svg 🔗

@@ -1,4 +1 @@
-<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>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize-2"><polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" x2="14" y1="3" y2="10"/><line x1="3" x2="10" y1="21" y2="14"/></svg>

assets/icons/menu.svg 🔗

@@ -1,3 +1 @@
-<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 3C1.22386 3 1 3.22386 1 3.5C1 3.77614 1.22386 4 1.5 4H13.5C13.7761 4 14 3.77614 14 3.5C14 3.22386 13.7761 3 13.5 3H1.5ZM1 7.5C1 7.22386 1.22386 7 1.5 7H13.5C13.7761 7 14 7.22386 14 7.5C14 7.77614 13.7761 8 13.5 8H1.5C1.22386 8 1 7.77614 1 7.5ZM1 11.5C1 11.2239 1.22386 11 1.5 11H13.5C13.7761 11 14 11.2239 14 11.5C14 11.7761 13.7761 12 13.5 12H1.5C1.22386 12 1 11.7761 1 11.5Z" fill="#CCCAC2"/>
-</svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-menu"><line x1="4" x2="20" y1="12" y2="12"/><line x1="4" x2="20" y1="6" y2="6"/><line x1="4" x2="20" y1="18" y2="18"/></svg>

assets/icons/minimize.svg 🔗

@@ -1,4 +1 @@
-<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>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-minimize-2"><polyline points="4 14 10 14 10 20"/><polyline points="20 10 14 10 14 4"/><line x1="14" x2="21" y1="10" y2="3"/><line x1="3" x2="10" y1="21" y2="14"/></svg>

assets/icons/quote.svg 🔗

@@ -1,8 +1 @@
-<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
-  <path
-    fill-rule="evenodd"
-    clip-rule="evenodd"

assets/icons/snip.svg 🔗

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-scissors"><circle cx="6" cy="6" r="3"/><path d="M8.12 8.12 12 12"/><path d="M20 4 8.12 15.88"/><circle cx="6" cy="18" r="3"/><path d="M14.8 14.8 20 20"/></svg>

crates/assistant2/src/assistant_panel.rs 🔗

@@ -54,7 +54,9 @@ use std::{
 };
 use theme::ThemeSettings;
 use ui::{
-    h_stack, prelude::*, v_stack, Button, ButtonLike, Icon, IconButton, IconElement, Label, Tooltip,
+    prelude::*,
+    utils::{DateTimeType, FormatDistance},
+    ButtonLike, Tab, TabBar, Tooltip,
 };
 use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt};
 use uuid::Uuid;
@@ -939,7 +941,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<Self>) -> Vec<AnyElement> {
@@ -955,12 +957,13 @@ impl AssistantPanel {
     }
 
     fn render_split_button(cx: &mut ViewContext<Self>) -> 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 +974,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 +989,7 @@ impl AssistantPanel {
                     });
                 }
             }))
+            .icon_size(IconSize::Small)
             .tooltip(|cx| Tooltip::for_action("Quote Seleciton", &QuoteSelection, cx))
     }
 
@@ -993,15 +998,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<Self>) -> 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)
             })
@@ -1020,10 +1029,19 @@ 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()))
+            .full_width()
+            .child(
+                div()
+                    .flex()
+                    .w_full()
+                    .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<Self>) -> Task<Result<()>> {
@@ -1112,20 +1130,35 @@ 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()
-                .child(Self::render_hamburger_button(cx))
-                .children(title);
-
-            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));
-            }
+            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(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_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()
+                });
 
             v_stack()
                 .size_full()
@@ -1165,8 +1198,6 @@ impl Render for AssistantPanel {
                             .into_any_element()
                         }),
                 )
-                .border()
-                .border_color(gpui::red())
         }
     }
 }
@@ -2251,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| {
@@ -2265,10 +2304,22 @@ impl ConversationEditor {
 
                             h_stack()
                                 .id(("message_header", message_id.0))
-                                .border()
-                                .border_color(gpui::red())
+                                .h_11()
+                                .gap_1()
+                                .p_1()
                                 .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(
@@ -2429,6 +2480,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)))
     }
@@ -2442,12 +2494,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))
     }
 }
 
@@ -2459,15 +2506,21 @@ impl Render for ConversationEditor {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> 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()

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
+    }
 }

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",

crates/ui2/src/components/tab.rs 🔗

@@ -48,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
@@ -94,8 +96,6 @@ impl RenderOnce for Tab {
     type Rendered = Stateful<Div>;
 
     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(Self::HEIGHT_IN_REMS))
             .bg(tab_bg)
             .border_color(cx.theme().colors().border)
             .map(|this| match self.position {

crates/ui2/src/utils/format_distance.rs 🔗

@@ -1,4 +1,72 @@
-use chrono::NaiveDateTime;
+use chrono::{DateTime, Local, NaiveDateTime};
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum DateTimeType {
+    Naive(NaiveDateTime),
+    Local(DateTime<Local>),
+}
+
+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<Local>, 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(),
+        }
+    }
+}
+
+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 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
+    }
+
+    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.
@@ -13,7 +81,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();
@@ -24,53 +97,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()
         }
     };
 
@@ -108,15 +256,16 @@ 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,
+    hide_prefix: 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)
+    distance_string(distance, include_seconds, add_suffix, hide_prefix)
 }
 
 /// Get the time difference between a date and now as relative human readable string.
@@ -142,14 +291,15 @@ 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,
+    hide_prefix: 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, hide_prefix)
 }
 
 #[cfg(test)]
@@ -158,73 +308,127 @@ mod tests {
     use chrono::NaiveDateTime;
 
     #[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");
+    fn test_format_distance() {
+        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, 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");
+    fn test_format_distance_with_suffix() {
+        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, false)
         );
     }
 
     #[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");
+    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"),
+        );
 
         assert_eq!(
             "over 54 years ago",
-            naive_format_distance_from_now(date, false, true)
+            format_distance_from_now(date, false, true, false)
         );
     }
 
     #[test]
-    fn test_naive_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");
+    fn test_format_distance_string() {
+        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_naive_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");
+    fn test_format_distance_string_include_seconds() {
+        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");
     }
 }