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