DEV: Group PMs by date (#1287)

# Preview

https://github.com/user-attachments/assets/3fe3ac8f-c938-4df4-9afe-11980046944d

# Details

- Group pms by `last_posted_at`. In this first iteration we are group by `7 days`, `30 days`, then by month beyond that. 
- I inject a sidebar section link with the relative (last_posted_at) date and then update a tracked value to ensure we don't do it again. Then for each month beyond the first 30days, I add a value to the `loadedMonthLabels` set and we reference that (plus the year) to see if we need to load a new month label.
- I took the creative liberty to remove the `Conversations` section label - this had no purpose
- I hid the _collapse all sidebar sections_ carrot. This had no purpose. 
- Swap `BasicTopicSerializer` to `ListableTopicSerializer` to get access to `last_posted_at`
This commit is contained in:
Isaac Janzen 2025-04-25 13:20:18 -05:00 committed by GitHub
parent 60ea590ba5
commit cd0cfc0bfc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 126 additions and 22 deletions

View File

@ -10,7 +10,6 @@ module DiscourseAi
page = params[:page].to_i
per_page = params[:per_page]&.to_i || 40
bot_user_ids = EntryPoint.all_bot_ids
base_query =
Topic
.private_messages_for_user(current_user)
@ -26,7 +25,7 @@ module DiscourseAi
pms = base_query.order(last_posted_at: :desc).offset(page * per_page).limit(per_page)
render json: {
conversations: serialize_data(pms, BasicTopicSerializer),
conversations: serialize_data(pms, ListableTopicSerializer),
meta: {
total: total,
page: page,

View File

@ -1,7 +1,9 @@
import { tracked } from "@glimmer/tracking";
import { htmlSafe } from "@ember/template";
import { TrackedArray } from "@ember-compat/tracked-built-ins";
import { ajax } from "discourse/lib/ajax";
import { bind } from "discourse/lib/decorators";
import { autoUpdatingRelativeAge } from "discourse/lib/formatter";
import { withPluginApi } from "discourse/lib/plugin-api";
import { i18n } from "discourse-i18n";
import AiBotSidebarNewConversation from "../discourse/components/ai-bot-sidebar-new-conversation";
@ -85,6 +87,9 @@ export default {
@tracked links = new TrackedArray();
@tracked topics = [];
@tracked hasMore = [];
@tracked loadedSevenDayLabel = false;
@tracked loadedThirtyDayLabel = false;
@tracked loadedMonthLabels = new Set();
page = 0;
isFetching = false;
totalTopicsCount = 0;
@ -127,7 +132,14 @@ export default {
}
addNewPMToSidebar(topic) {
this.links = [new AiConversationLink(topic), ...this.links];
// Reset category labels since we're adding a new topic
this.loadedSevenDayLabel = false;
this.loadedThirtyDayLabel = false;
this.loadedMonthLabels.clear();
this.topics = [topic, ...this.topics];
this.buildSidebarLinks();
this.watchForTitleUpdate(topic);
}
@ -206,10 +218,71 @@ export default {
this.fetchMessages(true);
}
buildSidebarLinks() {
this.links = this.topics.map(
(topic) => new AiConversationLink(topic)
groupByDate(topic) {
const now = new Date();
const lastPostedAt = new Date(topic.last_posted_at);
const daysDiff = Math.round(
(now - lastPostedAt) / (1000 * 60 * 60 * 24)
);
// Last 7 days group
if (daysDiff <= 7) {
if (!this.loadedSevenDayLabel) {
this.loadedSevenDayLabel = true;
return {
text: i18n("discourse_ai.ai_bot.conversations.last_7_days"),
classNames: "date-heading",
name: "date-heading-last-7-days",
};
}
}
// Last 30 days group
else if (daysDiff <= 30) {
if (!this.loadedThirtyDayLabel) {
this.loadedThirtyDayLabel = true;
return {
text: i18n(
"discourse_ai.ai_bot.conversations.last_30_days"
),
classNames: "date-heading",
name: "date-heading-last-30-days",
};
}
}
// Group by month for older conversations
else {
const month = lastPostedAt.getMonth();
const year = lastPostedAt.getFullYear();
const monthKey = `${year}-${month}`;
if (!this.loadedMonthLabels.has(monthKey)) {
this.loadedMonthLabels.add(monthKey);
const formattedDate = autoUpdatingRelativeAge(
new Date(topic.last_posted_at)
);
return {
text: htmlSafe(formattedDate),
classNames: "date-heading",
name: `date-heading-${monthKey}`,
};
}
}
}
buildSidebarLinks() {
// Reset date header tracking
this.loadedSevenDayLabel = false;
this.loadedThirtyDayLabel = false;
this.loadedMonthLabels.clear();
this.links = [...this.topics].flatMap((topic) => {
const dateLabel = this.groupByDate(topic);
return dateLabel
? [dateLabel, new AiConversationLink(topic)]
: [new AiConversationLink(topic)];
});
}
watchForTitleUpdate(topic) {

View File

@ -10,6 +10,10 @@ body.has-ai-conversations-sidebar {
margin: 1.8em 1rem 0;
}
.sidebar-toggle-all-sections {
display: none;
}
.sidebar-wrapper {
.ai-conversations-panel {
padding-top: 1em;
@ -18,19 +22,23 @@ body.has-ai-conversations-sidebar {
// ai related sidebar content
[data-section-name="ai-conversations-history"] {
.sidebar-section-header-wrapper {
pointer-events: none;
font-size: var(--font-down-1);
.sidebar-section-header-caret {
display: none;
}
.sidebar-section-header-text {
letter-spacing: 0.5px;
}
display: none;
}
.sidebar-section-link-wrapper {
.sidebar-section-link.date-heading {
pointer-events: none;
cursor: default;
color: var(--primary-medium);
opacity: 0.8;
font-weight: 700;
margin-top: 1em;
&[data-link-name="date-heading-last-7-days"] {
margin-top: 0;
}
}
.sidebar-section-link {
height: unset;
padding-block: 0.65em;

View File

@ -727,6 +727,8 @@ en:
new: "New Question"
min_input_length_message: "Message must be longer than 10 characters"
messages_sidebar_title: "Conversations"
last_7_days: "Last 7 days"
last_30_days: "Last 30 days"
sentiments:
dashboard:
title: "Sentiment"

View File

@ -49,6 +49,7 @@ RSpec.describe "AI Bot - Homepage", type: :system do
:private_message_topic,
title: "This is my special PM",
user: user,
last_posted_at: Time.zone.now,
topic_allowed_users: [
Fabricate.build(:topic_allowed_user, user: user),
Fabricate.build(:topic_allowed_user, user: bot_user),
@ -150,18 +151,35 @@ RSpec.describe "AI Bot - Homepage", type: :system do
expect(ai_pm_homepage).to have_homepage
expect(sidebar).to have_section("ai-conversations-history")
expect(sidebar).to have_section_link("Last 7 days")
expect(sidebar).to have_section_link(pm.title)
expect(sidebar).to have_no_css("button.ai-new-question-button")
end
it "displays last_30_days label in the sidebar" do
pm.update!(last_posted_at: Time.zone.now - 28.days)
visit "/"
header.click_bot_button
expect(ai_pm_homepage).to have_homepage
expect(sidebar).to have_section_link("Last 30 days")
end
it "displays month and year label in the sidebar for older conversations" do
pm.update!(last_posted_at: "2024-04-10 15:39:11.406192000 +00:00")
visit "/"
header.click_bot_button
expect(ai_pm_homepage).to have_homepage
expect(sidebar).to have_section_link("Apr 2024")
end
it "navigates to the bot conversation when clicked" do
visit "/"
header.click_bot_button
expect(ai_pm_homepage).to have_homepage
sidebar.find(
".sidebar-section[data-section-name='ai-conversations-history'] a.sidebar-section-link",
).click
ai_pm_homepage.click_fist_sidebar_conversation
expect(topic_page).to have_topic_title(pm.title)
end
@ -173,9 +191,7 @@ RSpec.describe "AI Bot - Homepage", type: :system do
expect(header).to have_icon_in_bot_button(icon: "shuffle")
# Go to a PM and assert that the icon is still shuffle
sidebar.find(
".sidebar-section[data-section-name='ai-conversations-history'] a.sidebar-section-link",
).click
ai_pm_homepage.click_fist_sidebar_conversation
expect(header).to have_icon_in_bot_button(icon: "shuffle")
# Go back home and assert that the icon is now robot again

View File

@ -40,6 +40,12 @@ module PageObjects
page.find(".ai-new-question-button").click
end
def click_fist_sidebar_conversation
page.find(
".sidebar-section[data-section-name='ai-conversations-history'] a.sidebar-section-link:not(.date-heading)",
).click
end
def persona_selector
PageObjects::Components::SelectKit.new(".persona-llm-selector__persona-dropdown")
end