1use gpui::{
2 uniform_list, AppContext, FocusHandle, FocusableView, Model, UniformListScrollHandle, WeakView,
3};
4use time::{OffsetDateTime, UtcOffset};
5use ui::{prelude::*, IconButtonShape, ListItem};
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 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 const DEFAULT_SUMMARY: SharedString = SharedString::new_static("New Thread");
104 let thread = self.thread.read(cx);
105 (
106 thread.id().clone(),
107 thread.summary().unwrap_or(DEFAULT_SUMMARY),
108 )
109 };
110
111 let thread_timestamp = time_format::format_localized_timestamp(
112 OffsetDateTime::from_unix_timestamp(self.thread.read(cx).updated_at().timestamp())
113 .unwrap(),
114 OffsetDateTime::now_utc(),
115 self.assistant_panel
116 .update(cx, |this, _cx| this.local_timezone())
117 .unwrap_or(UtcOffset::UTC),
118 time_format::TimestampFormat::EnhancedAbsolute,
119 );
120 ListItem::new(("past-thread", self.thread.entity_id()))
121 .start_slot(Icon::new(IconName::MessageBubbles))
122 .child(Label::new(summary))
123 .end_slot(
124 h_flex()
125 .gap_2()
126 .child(Label::new(thread_timestamp).color(Color::Disabled))
127 .child(
128 IconButton::new("delete", IconName::TrashAlt)
129 .shape(IconButtonShape::Square)
130 .icon_size(IconSize::Small)
131 .on_click({
132 let assistant_panel = self.assistant_panel.clone();
133 let id = id.clone();
134 move |_event, cx| {
135 assistant_panel
136 .update(cx, |this, cx| {
137 this.delete_thread(&id, cx);
138 })
139 .ok();
140 }
141 }),
142 ),
143 )
144 .on_click({
145 let assistant_panel = self.assistant_panel.clone();
146 let id = id.clone();
147 move |_event, cx| {
148 assistant_panel
149 .update(cx, |this, cx| {
150 this.open_thread(&id, cx);
151 })
152 .ok();
153 }
154 })
155 }
156}