1use gpui::{
2 uniform_list, AppContext, FocusHandle, FocusableView, Model, UniformListScrollHandle, WeakView,
3};
4use time::{OffsetDateTime, UtcOffset};
5use ui::{prelude::*, IconButtonShape, ListItem, ListItemSpacing, Tooltip};
6
7use crate::thread::Thread;
8use crate::thread_store::ThreadStore;
9use crate::AssistantPanel;
10
11pub struct ThreadHistory {
12 focus_handle: FocusHandle,
13 assistant_panel: WeakView<AssistantPanel>,
14 thread_store: Model<ThreadStore>,
15 scroll_handle: UniformListScrollHandle,
16}
17
18impl ThreadHistory {
19 pub(crate) fn new(
20 assistant_panel: WeakView<AssistantPanel>,
21 thread_store: Model<ThreadStore>,
22 cx: &mut ViewContext<Self>,
23 ) -> Self {
24 Self {
25 focus_handle: cx.focus_handle(),
26 assistant_panel,
27 thread_store,
28 scroll_handle: UniformListScrollHandle::default(),
29 }
30 }
31}
32
33impl FocusableView for ThreadHistory {
34 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
35 self.focus_handle.clone()
36 }
37}
38
39impl Render for ThreadHistory {
40 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
41 let threads = self.thread_store.update(cx, |this, cx| this.threads(cx));
42
43 v_flex()
44 .id("thread-history-container")
45 .track_focus(&self.focus_handle)
46 .overflow_y_scroll()
47 .size_full()
48 .p_1()
49 .map(|history| {
50 if threads.is_empty() {
51 history
52 .justify_center()
53 .child(
54 h_flex().w_full().justify_center().child(
55 Label::new("You don't have any past threads yet.")
56 .size(LabelSize::Small),
57 ),
58 )
59 } else {
60 history.child(
61 uniform_list(
62 cx.view().clone(),
63 "thread-history",
64 threads.len(),
65 move |history, range, _cx| {
66 threads[range]
67 .iter()
68 .map(|thread| {
69 h_flex().w_full().pb_1().child(PastThread::new(
70 thread.clone(),
71 history.assistant_panel.clone(),
72 ))
73 })
74 .collect()
75 },
76 )
77 .track_scroll(self.scroll_handle.clone())
78 .flex_grow(),
79 )
80 }
81 })
82 }
83}
84
85#[derive(IntoElement)]
86pub struct PastThread {
87 thread: Model<Thread>,
88 assistant_panel: WeakView<AssistantPanel>,
89}
90
91impl PastThread {
92 pub fn new(thread: Model<Thread>, assistant_panel: WeakView<AssistantPanel>) -> Self {
93 Self {
94 thread,
95 assistant_panel,
96 }
97 }
98}
99
100impl RenderOnce for PastThread {
101 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
102 let (id, summary) = {
103 let thread = self.thread.read(cx);
104 (thread.id().clone(), thread.summary_or_default())
105 };
106
107 let thread_timestamp = time_format::format_localized_timestamp(
108 OffsetDateTime::from_unix_timestamp(self.thread.read(cx).updated_at().timestamp())
109 .unwrap(),
110 OffsetDateTime::now_utc(),
111 self.assistant_panel
112 .update(cx, |this, _cx| this.local_timezone())
113 .unwrap_or(UtcOffset::UTC),
114 time_format::TimestampFormat::EnhancedAbsolute,
115 );
116
117 ListItem::new(("past-thread", self.thread.entity_id()))
118 .outlined()
119 .start_slot(
120 Icon::new(IconName::MessageCircle)
121 .size(IconSize::Small)
122 .color(Color::Muted),
123 )
124 .spacing(ListItemSpacing::Sparse)
125 .child(Label::new(summary).size(LabelSize::Small).text_ellipsis())
126 .end_slot(
127 h_flex()
128 .gap_2()
129 .child(
130 Label::new(thread_timestamp)
131 .color(Color::Disabled)
132 .size(LabelSize::Small),
133 )
134 .child(
135 IconButton::new("delete", IconName::TrashAlt)
136 .shape(IconButtonShape::Square)
137 .icon_size(IconSize::Small)
138 .tooltip(|cx| Tooltip::text("Delete Thread", cx))
139 .on_click({
140 let assistant_panel = self.assistant_panel.clone();
141 let id = id.clone();
142 move |_event, cx| {
143 assistant_panel
144 .update(cx, |this, cx| {
145 this.delete_thread(&id, cx);
146 })
147 .ok();
148 }
149 }),
150 ),
151 )
152 .on_click({
153 let assistant_panel = self.assistant_panel.clone();
154 let id = id.clone();
155 move |_event, cx| {
156 assistant_panel
157 .update(cx, |this, cx| {
158 this.open_thread(&id, cx);
159 })
160 .ok();
161 }
162 })
163 }
164}