11import { tracked } from "@glimmer/tracking" ;
2+ import { schedule } from "@ember/runloop" ;
23import Service , { service } from "@ember/service" ;
4+ import { htmlSafe } from "@ember/template" ;
5+ import { TrackedArray } from "@ember-compat/tracked-built-ins" ;
6+ import { ajax } from "discourse/lib/ajax" ;
7+ import { autoUpdatingRelativeAge } from "discourse/lib/formatter" ;
38import { ADMIN_PANEL , MAIN_PANEL } from "discourse/lib/sidebar/panels" ;
9+ import { i18n } from "discourse-i18n" ;
10+ import AiBotSidebarEmptyState from "../../discourse/components/ai-bot-sidebar-empty-state" ;
411
512export const AI_CONVERSATIONS_PANEL = "ai-conversations" ;
613
@@ -9,14 +16,31 @@ export default class AiConversationsSidebarManager extends Service {
916 @service sidebarState ;
1017
1118 @tracked newTopicForceSidebar = false ;
19+ @tracked sections = new TrackedArray ( ) ;
20+ @tracked isLoading = true ;
1221
13- forceCustomSidebar ( ) {
22+ isFetching = false ;
23+ page = 0 ;
24+ hasMore = true ;
25+
26+ loadedTodayLabel = false ;
27+ loadedSevenDayLabel = false ;
28+ loadedThirtyDayLabel = false ;
29+ loadedMonthLabels = new Set ( ) ;
30+
31+ forceCustomSidebar ( api ) {
1432 // Return early if we already have the correct panel, so we don't
1533 // re-render it.
34+
1635 if ( this . sidebarState . currentPanel ?. key === AI_CONVERSATIONS_PANEL ) {
1736 return ;
1837 }
1938
39+ schedule ( "afterRender" , async ( ) => {
40+ await this . fetchMessages ( api ) ;
41+ this . sidebarState . setPanel ( "ai-conversations" ) ;
42+ } ) ;
43+
2044 this . sidebarState . setPanel ( AI_CONVERSATIONS_PANEL ) ;
2145
2246 // Use separated mode to ensure independence from hamburger menu
@@ -45,4 +69,195 @@ export default class AiConversationsSidebarManager extends Service {
4569 this . appEvents . trigger ( "discourse-ai:stop-forcing-conversations-sidebar" ) ;
4670 }
4771 }
72+
73+ async fetchMessages ( api ) {
74+ if ( this . isFetching ) {
75+ return ;
76+ }
77+ this . isFetching = true ;
78+
79+ try {
80+ let { conversations, meta } = await ajax (
81+ "/discourse-ai/ai-bot/conversations.json" ,
82+ { data : { page : this . page , per_page : 40 } }
83+ ) ;
84+
85+ this . page += 1 ;
86+ this . hasMore = meta . has_more ;
87+
88+ // Append new topics and rebuild groups
89+ this . _topics = [ ...( this . _topics || [ ] ) , ...conversations ] ;
90+ } catch {
91+ this . isFetching = false ;
92+ this . isLoading = false ;
93+ } finally {
94+ this . isFetching = false ;
95+ this . isLoading = false ;
96+ this . buildSections ( api ) ;
97+ }
98+ }
99+
100+ buildSections ( api ) {
101+ // reset grouping flags
102+ this . loadedTodayLabel = false ;
103+ this . loadedSevenDayLabel = false ;
104+ this . loadedThirtyDayLabel = false ;
105+ this . loadedMonthLabels . clear ( ) ;
106+
107+ const now = new Date ( ) ;
108+ const sections = [ ] ;
109+ let currentSection = null ;
110+
111+ ( this . _topics || [ ] ) . forEach ( ( topic ) => {
112+ const heading = this . groupByDate ( topic , now ) ;
113+
114+ // new section for new heading
115+ if ( heading ) {
116+ currentSection = {
117+ title : heading . text ,
118+ name : heading . name ,
119+ classNames : heading . classNames ,
120+ links : new TrackedArray ( ) ,
121+ } ;
122+ sections . push ( currentSection ) ;
123+ }
124+
125+ // always add topic link under the latest section
126+ if ( currentSection ) {
127+ currentSection . links . push ( {
128+ route : "topic.fromParamsNear" ,
129+ models : [ topic . slug , topic . id , topic . last_read_post_number || 0 ] ,
130+ title : topic . title ,
131+ text : topic . title ,
132+ key : topic . id ,
133+ classNames : `ai-conversation-${ topic . id } ` ,
134+ } ) ;
135+ }
136+ } ) ;
137+
138+ this . sections = sections ;
139+
140+ this . mountSections ( api ) ;
141+ }
142+
143+ mountSections ( api ) {
144+ this . sections . forEach ( ( section ) => {
145+ api . addSidebarSection (
146+ ( BaseCustomSidebarSection , BaseCustomSidebarSectionLink ) => {
147+ return class extends BaseCustomSidebarSection {
148+ get name ( ) {
149+ return section . name ;
150+ }
151+
152+ get title ( ) {
153+ return section . title ;
154+ }
155+
156+ get text ( ) {
157+ return section . title ;
158+ }
159+
160+ get links ( ) {
161+ return section . links . map (
162+ ( link ) =>
163+ new ( class extends BaseCustomSidebarSectionLink {
164+ get name ( ) {
165+ return `conv-${ link . key } ` ;
166+ }
167+
168+ get route ( ) {
169+ return link . route ;
170+ }
171+
172+ get models ( ) {
173+ return link . models ;
174+ }
175+
176+ get title ( ) {
177+ return link . title ;
178+ }
179+
180+ get text ( ) {
181+ return link . text ;
182+ }
183+ } ) ( )
184+ ) ;
185+ }
186+
187+ get emptyStateComponent ( ) {
188+ if ( ! this . isLoading && section . links . length === 0 ) {
189+ return AiBotSidebarEmptyState ;
190+ }
191+ }
192+
193+ get sidebarElement ( ) {
194+ return document . querySelector (
195+ ".sidebar-wrapper .sidebar-sections"
196+ ) ;
197+ }
198+ } ;
199+ } ,
200+ AI_CONVERSATIONS_PANEL
201+ ) ;
202+ } ) ;
203+
204+ this . appEvents . trigger ( "discourse-ai:conversations-sidebar-updated" ) ;
205+ }
206+
207+ groupByDate ( topic , now = new Date ( ) ) {
208+ const lastPostedAt = new Date ( topic . last_posted_at ) ;
209+ const daysDiff = Math . round ( ( now - lastPostedAt ) / ( 1000 * 60 * 60 * 24 ) ) ;
210+
211+ // Today
212+ if ( daysDiff <= 1 || ! topic . last_posted_at ) {
213+ if ( ! this . loadedTodayLabel ) {
214+ this . loadedTodayLabel = true ;
215+ return {
216+ text : i18n ( "discourse_ai.ai_bot.conversations.today" ) ,
217+ classNames : "date-heading" ,
218+ name : "date-heading-today" ,
219+ } ;
220+ }
221+ }
222+ // Last 7 days
223+ else if ( daysDiff <= 7 ) {
224+ if ( ! this . loadedSevenDayLabel ) {
225+ this . loadedSevenDayLabel = true ;
226+ return {
227+ text : i18n ( "discourse_ai.ai_bot.conversations.last_7_days" ) ,
228+ classNames : "date-heading" ,
229+ name : "date-heading-last-7-days" ,
230+ } ;
231+ }
232+ }
233+ // Last 30 days
234+ else if ( daysDiff <= 30 ) {
235+ if ( ! this . loadedThirtyDayLabel ) {
236+ this . loadedThirtyDayLabel = true ;
237+ return {
238+ text : i18n ( "discourse_ai.ai_bot.conversations.last_30_days" ) ,
239+ classNames : "date-heading" ,
240+ name : "date-heading-last-30-days" ,
241+ } ;
242+ }
243+ }
244+ // Older: group by month
245+ else {
246+ const month = lastPostedAt . getMonth ( ) ;
247+ const year = lastPostedAt . getFullYear ( ) ;
248+ const monthKey = `${ year } -${ month } ` ;
249+
250+ if ( ! this . loadedMonthLabels . has ( monthKey ) ) {
251+ this . loadedMonthLabels . add ( monthKey ) ;
252+ const formattedDate = autoUpdatingRelativeAge ( lastPostedAt ) ;
253+ return {
254+ text : htmlSafe ( formattedDate ) ,
255+ classNames : "date-heading" ,
256+ name : `date-heading-${ monthKey } ` ,
257+ } ;
258+ }
259+ }
260+
261+ return null ;
262+ }
48263}
0 commit comments