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