1use gpui::{
2 uniform_list, App, Entity, FocusHandle, Focusable, ScrollStrategy, UniformListScrollHandle,
3 WeakEntity,
4};
5use time::{OffsetDateTime, UtcOffset};
6use ui::{prelude::*, IconButtonShape, ListItem, ListItemSpacing, Tooltip};
7
8use crate::thread_store::{SavedThreadMetadata, ThreadStore};
9use crate::{AssistantPanel, RemoveSelectedThread};
10
11pub struct ThreadHistory {
12 focus_handle: FocusHandle,
13 assistant_panel: WeakEntity<AssistantPanel>,
14 thread_store: Entity<ThreadStore>,
15 scroll_handle: UniformListScrollHandle,
16 selected_index: usize,
17}
18
19impl ThreadHistory {
20 pub(crate) fn new(
21 assistant_panel: WeakEntity<AssistantPanel>,
22 thread_store: Entity<ThreadStore>,
23 cx: &mut Context<Self>,
24 ) -> Self {
25 Self {
26 focus_handle: cx.focus_handle(),
27 assistant_panel,
28 thread_store,
29 scroll_handle: UniformListScrollHandle::default(),
30 selected_index: 0,
31 }
32 }
33
34 pub fn select_prev(
35 &mut self,
36 _: &menu::SelectPrev,
37 window: &mut Window,
38 cx: &mut Context<Self>,
39 ) {
40 let count = self.thread_store.read(cx).thread_count();
41 if count > 0 {
42 if self.selected_index == 0 {
43 self.set_selected_index(count - 1, window, cx);
44 } else {
45 self.set_selected_index(self.selected_index - 1, window, cx);
46 }
47 }
48 }
49
50 pub fn select_next(
51 &mut self,
52 _: &menu::SelectNext,
53 window: &mut Window,
54 cx: &mut Context<Self>,
55 ) {
56 let count = self.thread_store.read(cx).thread_count();
57 if count > 0 {
58 if self.selected_index == count - 1 {
59 self.set_selected_index(0, window, cx);
60 } else {
61 self.set_selected_index(self.selected_index + 1, window, cx);
62 }
63 }
64 }
65
66 fn select_first(&mut self, _: &menu::SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
67 let count = self.thread_store.read(cx).thread_count();
68 if count > 0 {
69 self.set_selected_index(0, window, cx);
70 }
71 }
72
73 fn select_last(&mut self, _: &menu::SelectLast, window: &mut Window, cx: &mut Context<Self>) {
74 let count = self.thread_store.read(cx).thread_count();
75 if count > 0 {
76 self.set_selected_index(count - 1, window, cx);
77 }
78 }
79
80 fn set_selected_index(&mut self, index: usize, _window: &mut Window, cx: &mut Context<Self>) {
81 self.selected_index = index;
82 self.scroll_handle
83 .scroll_to_item(index, ScrollStrategy::Top);
84 cx.notify();
85 }
86
87 fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
88 let threads = self.thread_store.update(cx, |this, _cx| this.threads());
89
90 if let Some(thread) = threads.get(self.selected_index) {
91 self.assistant_panel
92 .update(cx, move |this, cx| this.open_thread(&thread.id, window, cx))
93 .ok();
94
95 cx.notify();
96 }
97 }
98
99 fn remove_selected_thread(
100 &mut self,
101 _: &RemoveSelectedThread,
102 _window: &mut Window,
103 cx: &mut Context<Self>,
104 ) {
105 let threads = self.thread_store.update(cx, |this, _cx| this.threads());
106
107 if let Some(thread) = threads.get(self.selected_index) {
108 self.assistant_panel
109 .update(cx, |this, cx| {
110 this.delete_thread(&thread.id, cx);
111 })
112 .ok();
113
114 cx.notify();
115 }
116 }
117}
118
119impl Focusable for ThreadHistory {
120 fn focus_handle(&self, _cx: &App) -> FocusHandle {
121 self.focus_handle.clone()
122 }
123}
124
125impl Render for ThreadHistory {
126 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
127 let threads = self.thread_store.update(cx, |this, _cx| this.threads());
128 let selected_index = self.selected_index;
129
130 v_flex()
131 .id("thread-history-container")
132 .key_context("ThreadHistory")
133 .track_focus(&self.focus_handle)
134 .overflow_y_scroll()
135 .size_full()
136 .p_1()
137 .on_action(cx.listener(Self::select_prev))
138 .on_action(cx.listener(Self::select_next))
139 .on_action(cx.listener(Self::select_first))
140 .on_action(cx.listener(Self::select_last))
141 .on_action(cx.listener(Self::confirm))
142 .on_action(cx.listener(Self::remove_selected_thread))
143 .map(|history| {
144 if threads.is_empty() {
145 history
146 .justify_center()
147 .child(
148 h_flex().w_full().justify_center().child(
149 Label::new("You don't have any past threads yet.")
150 .size(LabelSize::Small),
151 ),
152 )
153 } else {
154 history.child(
155 uniform_list(
156 cx.entity().clone(),
157 "thread-history",
158 threads.len(),
159 move |history, range, _window, _cx| {
160 threads[range]
161 .iter()
162 .enumerate()
163 .map(|(index, thread)| {
164 h_flex().w_full().pb_1().child(PastThread::new(
165 thread.clone(),
166 history.assistant_panel.clone(),
167 selected_index == index,
168 ))
169 })
170 .collect()
171 },
172 )
173 .track_scroll(self.scroll_handle.clone())
174 .flex_grow(),
175 )
176 }
177 })
178 }
179}
180
181#[derive(IntoElement)]
182pub struct PastThread {
183 thread: SavedThreadMetadata,
184 assistant_panel: WeakEntity<AssistantPanel>,
185 selected: bool,
186}
187
188impl PastThread {
189 pub fn new(
190 thread: SavedThreadMetadata,
191 assistant_panel: WeakEntity<AssistantPanel>,
192 selected: bool,
193 ) -> Self {
194 Self {
195 thread,
196 assistant_panel,
197 selected,
198 }
199 }
200}
201
202impl RenderOnce for PastThread {
203 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
204 let summary = self.thread.summary;
205
206 let thread_timestamp = time_format::format_localized_timestamp(
207 OffsetDateTime::from_unix_timestamp(self.thread.updated_at.timestamp()).unwrap(),
208 OffsetDateTime::now_utc(),
209 self.assistant_panel
210 .update(cx, |this, _cx| this.local_timezone())
211 .unwrap_or(UtcOffset::UTC),
212 time_format::TimestampFormat::EnhancedAbsolute,
213 );
214
215 ListItem::new(SharedString::from(self.thread.id.to_string()))
216 .outlined()
217 .toggle_state(self.selected)
218 .start_slot(
219 Icon::new(IconName::MessageCircle)
220 .size(IconSize::Small)
221 .color(Color::Muted),
222 )
223 .spacing(ListItemSpacing::Sparse)
224 .child(Label::new(summary).size(LabelSize::Small).text_ellipsis())
225 .end_slot(
226 h_flex()
227 .gap_1p5()
228 .child(
229 Label::new(thread_timestamp)
230 .color(Color::Muted)
231 .size(LabelSize::XSmall),
232 )
233 .child(
234 IconButton::new("delete", IconName::TrashAlt)
235 .shape(IconButtonShape::Square)
236 .icon_size(IconSize::XSmall)
237 .tooltip(Tooltip::text("Delete Thread"))
238 .on_click({
239 let assistant_panel = self.assistant_panel.clone();
240 let id = self.thread.id.clone();
241 move |_event, _window, cx| {
242 assistant_panel
243 .update(cx, |this, cx| {
244 this.delete_thread(&id, cx);
245 })
246 .ok();
247 }
248 }),
249 ),
250 )
251 .on_click({
252 let assistant_panel = self.assistant_panel.clone();
253 let id = self.thread.id.clone();
254 move |_event, window, cx| {
255 assistant_panel
256 .update(cx, |this, cx| {
257 this.open_thread(&id, window, cx).detach_and_log_err(cx);
258 })
259 .ok();
260 }
261 })
262 }
263}