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