Skip to content
This repository was archived by the owner on Jul 22, 2025. It is now read-only.

Commit 4eb05e6

Browse files
committed
WIP: use separate sidebar sections for AI conversation history
1 parent d99c335 commit 4eb05e6

File tree

2 files changed

+219
-301
lines changed

2 files changed

+219
-301
lines changed

assets/javascripts/discourse/services/ai-conversations-sidebar-manager.js

Lines changed: 216 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
import { tracked } from "@glimmer/tracking";
2+
import { schedule } from "@ember/runloop";
23
import 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";
38
import { 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

512
export 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

Comments
 (0)