mirror of
https://github.com/discourse/discourse-ai.git
synced 2025-12-21 06:53:35 +00:00
Context in dev conversation -- in hamburger mode we don't need the robot icon to swap to shuffle, AND we don't need back to forum link.
361 lines
12 KiB
JavaScript
361 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 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_enable_experimental_bot_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 = !navigationMenu.isHeaderDropdownMode;
|
|
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();
|
|
page = 0;
|
|
isFetching = false;
|
|
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 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;
|
|
}
|
|
}
|
|
|
|
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);
|
|
});
|
|
},
|
|
};
|