discourse-ai/assets/javascripts/initializers/ai-conversations-sidebar.js

371 lines
12 KiB
JavaScript

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 AiBotSidebarEmptyState from "../discourse/components/ai-bot-sidebar-empty-state";
import AiBotSidebarNewConversation from "../discourse/components/ai-bot-sidebar-new-conversation";
import { AI_CONVERSATIONS_PANEL } from "../discourse/services/ai-conversations-sidebar-manager";
export default {
name: "ai-conversations-sidebar",
initialize() {
withPluginApi((api) => {
const siteSettings = api.container.lookup("service:site-settings");
if (!siteSettings.ai_bot_enable_dedicated_ux) {
return;
}
const currentUser = api.container.lookup("service:current-user");
if (!currentUser) {
return;
}
const aiConversationsSidebarManager = api.container.lookup(
"service:ai-conversations-sidebar-manager"
);
const appEvents = api.container.lookup("service:app-events");
const messageBus = api.container.lookup("service:message-bus");
const navigationMenu = api.container.lookup("service:navigationMenu");
api.addSidebarPanel(
(BaseCustomSidebarPanel) =>
class AiConversationsSidebarPanel extends BaseCustomSidebarPanel {
key = AI_CONVERSATIONS_PANEL;
hidden = true;
displayHeader = false; // this would add a misplaced back to forum button
expandActiveSection = true;
}
);
api.renderInOutlet(
"before-sidebar-sections",
AiBotSidebarNewConversation
);
api.addSidebarSection(
(BaseCustomSidebarSection, BaseCustomSidebarSectionLink) => {
const AiConversationLink = class extends BaseCustomSidebarSectionLink {
route = "topic.fromParamsNear";
constructor(topic) {
super(...arguments);
this.topic = topic;
}
get key() {
return this.topic.id;
}
get name() {
return this.topic.title;
}
get models() {
return [
this.topic.slug,
this.topic.id,
this.topic.last_read_post_number || 0,
];
}
get title() {
return this.topic.title;
}
get text() {
return this.topic.title;
}
get classNames() {
return `ai-conversation-${this.topic.id}`;
}
};
return class extends BaseCustomSidebarSection {
@tracked links = new TrackedArray();
@tracked topics = [];
@tracked hasMore = [];
@tracked loadedTodayLabel = false;
@tracked loadedSevenDayLabel = false;
@tracked loadedThirtyDayLabel = false;
@tracked loadedMonthLabels = new Set();
@tracked isLoading = true;
isFetching = false;
page = 0;
totalTopicsCount = 0;
constructor() {
super(...arguments);
this.fetchMessages();
appEvents.on(
"discourse-ai:bot-pm-created",
this,
"addNewPMToSidebar"
);
}
@bind
willDestroy() {
this.removeScrollListener();
appEvents.off(
"discourse-ai:bot-pm-created",
this,
"addNewPMToSidebar"
);
}
get name() {
return "ai-conversations-history";
}
get emptyStateComponent() {
if (!this.isLoading) {
return AiBotSidebarEmptyState;
}
}
get text() {
return i18n(
"discourse_ai.ai_bot.conversations.messages_sidebar_title"
);
}
get sidebarElement() {
return document.querySelector(
".sidebar-wrapper .sidebar-sections"
);
}
addNewPMToSidebar(topic) {
// Reset category labels since we're adding a new topic
this.loadedTodayLabel = false;
this.loadedSevenDayLabel = false;
this.loadedThirtyDayLabel = false;
this.loadedMonthLabels.clear();
this.topics = [topic, ...this.topics];
this.buildSidebarLinks();
this.watchForTitleUpdate(topic);
}
@bind
removeScrollListener() {
const sidebar = this.sidebarElement;
if (sidebar) {
sidebar.removeEventListener("scroll", this.scrollHandler);
}
}
@bind
attachScrollListener() {
const sidebar = this.sidebarElement;
if (sidebar) {
sidebar.addEventListener("scroll", this.scrollHandler);
}
}
@bind
scrollHandler() {
const sidebarElement = this.sidebarElement;
if (!sidebarElement) {
return;
}
const scrollPosition = sidebarElement.scrollTop;
const scrollHeight = sidebarElement.scrollHeight;
const clientHeight = sidebarElement.clientHeight;
// When user has scrolled to bottom with a small threshold
if (scrollHeight - scrollPosition - clientHeight < 100) {
if (this.hasMore && !this.isFetching) {
this.loadMore();
}
}
}
async fetchMessages(isLoadingMore = false) {
if (this.isFetching) {
return;
}
try {
this.isFetching = true;
const data = await ajax(
"/discourse-ai/ai-bot/conversations.json",
{
data: { page: this.page, per_page: 40 },
}
);
if (isLoadingMore) {
this.topics = [...this.topics, ...data.conversations];
} else {
this.topics = data.conversations;
}
this.totalTopicsCount = data.meta.total;
this.hasMore = data.meta.has_more;
this.isFetching = false;
this.removeScrollListener();
this.buildSidebarLinks();
this.attachScrollListener();
} catch {
this.isFetching = false;
} finally {
this.isLoading = false;
}
}
loadMore() {
if (this.isFetching || !this.hasMore) {
return;
}
this.page = this.page + 1;
this.fetchMessages(true);
}
groupByDate(topic) {
const now = new Date();
const lastPostedAt = new Date(topic.last_posted_at);
const daysDiff = Math.round(
(now - lastPostedAt) / (1000 * 60 * 60 * 24)
);
if (daysDiff <= 1 || !topic.last_posted_at) {
if (!this.loadedTodayLabel) {
this.loadedTodayLabel = true;
return {
text: i18n("discourse_ai.ai_bot.conversations.today"),
classNames: "date-heading",
name: "date-heading-today",
};
}
}
// Last 7 days group
else 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.loadedTodayLabel = false;
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) {
const channel = `/discourse-ai/ai-bot/topic/${topic.id}`;
const callback = this.updateTopicTitle.bind(this);
messageBus.subscribe(channel, ({ title }) => {
callback(topic, title);
messageBus.unsubscribe(channel);
});
}
updateTopicTitle(topic, title) {
// update the data
topic.title = title;
// force Glimmer to re-render that one link
this.links = this.links.map((link) =>
link?.topic?.id === topic.id
? new AiConversationLink(topic)
: link
);
}
};
},
AI_CONVERSATIONS_PANEL
);
const setSidebarPanel = (transition) => {
if (transition?.to?.name === "discourse-ai-bot-conversations") {
return aiConversationsSidebarManager.forceCustomSidebar();
}
const topic = api.container.lookup("controller:topic").model;
// if the topic is not a private message, not created by the current user,
// or doesn't have a bot response, we don't need to override sidebar
if (
topic?.archetype === "private_message" &&
topic.user_id === currentUser.id &&
topic.is_bot_pm
) {
return aiConversationsSidebarManager.forceCustomSidebar();
}
// newTopicForceSidebar is set to true when a new topic is created. We have
// this because the condition `postStream.posts` above will not be true as the bot response
// is not in the postStream yet when this initializer is ran. So we need to force
// the sidebar to open when creating a new topic. After that, we set it to false again.
if (aiConversationsSidebarManager.newTopicForceSidebar) {
aiConversationsSidebarManager.newTopicForceSidebar = false;
return aiConversationsSidebarManager.forceCustomSidebar();
}
aiConversationsSidebarManager.stopForcingCustomSidebar();
};
api.container
.lookup("service:router")
.on("routeDidChange", setSidebarPanel);
});
},
};