mirror of
https://github.com/discourse/discourse-ai.git
synced 2025-06-25 00:52:14 +00:00
REFACTOR: update AI conversation sidebar to use sidebar sections for date grouping (#1389)
This commit is contained in:
parent
306fec2b24
commit
fa51e9d948
@ -1,48 +1,317 @@
|
|||||||
import { tracked } from "@glimmer/tracking";
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
import { scheduleOnce } from "@ember/runloop";
|
||||||
import Service, { service } from "@ember/service";
|
import Service, { service } from "@ember/service";
|
||||||
|
import { htmlSafe } from "@ember/template";
|
||||||
|
import { TrackedArray } from "@ember-compat/tracked-built-ins";
|
||||||
|
import { ajax } from "discourse/lib/ajax";
|
||||||
|
import discourseDebounce from "discourse/lib/debounce";
|
||||||
|
import { autoUpdatingRelativeAge } from "discourse/lib/formatter";
|
||||||
import { ADMIN_PANEL, MAIN_PANEL } from "discourse/lib/sidebar/panels";
|
import { ADMIN_PANEL, MAIN_PANEL } from "discourse/lib/sidebar/panels";
|
||||||
|
import { i18n } from "discourse-i18n";
|
||||||
|
import AiBotSidebarEmptyState from "../../discourse/components/ai-bot-sidebar-empty-state";
|
||||||
|
|
||||||
export const AI_CONVERSATIONS_PANEL = "ai-conversations";
|
export const AI_CONVERSATIONS_PANEL = "ai-conversations";
|
||||||
|
const SCROLL_BUFFER = 100;
|
||||||
|
const DEBOUNCE = 100;
|
||||||
|
|
||||||
export default class AiConversationsSidebarManager extends Service {
|
export default class AiConversationsSidebarManager extends Service {
|
||||||
@service appEvents;
|
@service appEvents;
|
||||||
@service sidebarState;
|
@service sidebarState;
|
||||||
|
@service messageBus;
|
||||||
|
|
||||||
@tracked newTopicForceSidebar = false;
|
@tracked topics = [];
|
||||||
|
@tracked sections = new TrackedArray();
|
||||||
|
@tracked isLoading = true;
|
||||||
|
|
||||||
|
api = null;
|
||||||
|
isFetching = false;
|
||||||
|
page = 0;
|
||||||
|
hasMore = true;
|
||||||
|
_registered = new Set();
|
||||||
|
_hasScrollListener = false;
|
||||||
|
_scrollElement = null;
|
||||||
|
_didInit = false;
|
||||||
|
|
||||||
|
_debouncedScrollHandler = () => {
|
||||||
|
discourseDebounce(
|
||||||
|
this,
|
||||||
|
() => {
|
||||||
|
const element = this._scrollElement;
|
||||||
|
if (!element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } = element;
|
||||||
|
if (
|
||||||
|
scrollHeight - scrollTop - clientHeight - SCROLL_BUFFER < 100 &&
|
||||||
|
!this.isFetching &&
|
||||||
|
this.hasMore
|
||||||
|
) {
|
||||||
|
this.fetchMessages();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
DEBOUNCE
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
|
||||||
|
this.appEvents.on(
|
||||||
|
"discourse-ai:bot-pm-created",
|
||||||
|
this,
|
||||||
|
this._handleNewBotPM
|
||||||
|
);
|
||||||
|
|
||||||
|
this.appEvents.on(
|
||||||
|
"discourse-ai:conversations-sidebar-updated",
|
||||||
|
this,
|
||||||
|
this._attachScrollListener
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
willDestroy() {
|
||||||
|
super.willDestroy(...arguments);
|
||||||
|
this.appEvents.off(
|
||||||
|
"discourse-ai:bot-pm-created",
|
||||||
|
this,
|
||||||
|
this._handleNewBotPM
|
||||||
|
);
|
||||||
|
this.appEvents.off(
|
||||||
|
"discourse-ai:conversations-sidebar-updated",
|
||||||
|
this,
|
||||||
|
this._attachScrollListener
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
forceCustomSidebar() {
|
forceCustomSidebar() {
|
||||||
// Return early if we already have the correct panel, so we don't
|
document.body.classList.add("has-ai-conversations-sidebar");
|
||||||
// re-render it.
|
this.sidebarState.isForcingSidebar = true;
|
||||||
if (this.sidebarState.currentPanel?.key === AI_CONVERSATIONS_PANEL) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// calling this before fetching data
|
||||||
|
// helps avoid flash of main sidebar mode
|
||||||
this.sidebarState.setPanel(AI_CONVERSATIONS_PANEL);
|
this.sidebarState.setPanel(AI_CONVERSATIONS_PANEL);
|
||||||
|
|
||||||
// Use separated mode to ensure independence from hamburger menu
|
this.appEvents.trigger("discourse-ai:force-conversations-sidebar");
|
||||||
this.sidebarState.setSeparatedMode();
|
this.sidebarState.setSeparatedMode();
|
||||||
|
|
||||||
// Hide panel switching buttons to keep UI clean
|
|
||||||
this.sidebarState.hideSwitchPanelButtons();
|
this.sidebarState.hideSwitchPanelButtons();
|
||||||
|
|
||||||
this.sidebarState.isForcingSidebar = true;
|
// don't render sidebar multiple times
|
||||||
document.body.classList.add("has-ai-conversations-sidebar");
|
if (this._didInit) {
|
||||||
this.appEvents.trigger("discourse-ai:force-conversations-sidebar");
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._didInit = true;
|
||||||
|
|
||||||
|
this.fetchMessages().then(() => {
|
||||||
|
this.sidebarState.setPanel(AI_CONVERSATIONS_PANEL);
|
||||||
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_attachScrollListener() {
|
||||||
|
const sections = document.querySelector(
|
||||||
|
".sidebar-sections.ai-conversations-panel"
|
||||||
|
);
|
||||||
|
this._scrollElement = sections;
|
||||||
|
|
||||||
|
if (this._hasScrollListener || !this._scrollElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sections.addEventListener("scroll", this._debouncedScrollHandler);
|
||||||
|
|
||||||
|
this._hasScrollListener = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_removeScrollListener() {
|
||||||
|
if (this._hasScrollListener) {
|
||||||
|
this._scrollElement.removeEventListener(
|
||||||
|
"scroll",
|
||||||
|
this._debouncedScrollHandler
|
||||||
|
);
|
||||||
|
this._hasScrollListener = false;
|
||||||
|
this._scrollElement = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
stopForcingCustomSidebar() {
|
stopForcingCustomSidebar() {
|
||||||
// This method is called when leaving your route
|
|
||||||
// Only restore main panel if we previously forced ours
|
|
||||||
document.body.classList.remove("has-ai-conversations-sidebar");
|
document.body.classList.remove("has-ai-conversations-sidebar");
|
||||||
const isAdminSidebarActive =
|
|
||||||
this.sidebarState.currentPanel?.key === ADMIN_PANEL;
|
const isAdmin = this.sidebarState.currentPanel?.key === ADMIN_PANEL;
|
||||||
// only restore main panel if we previously forced our sidebar
|
if (this.sidebarState.isForcingSidebar && !isAdmin) {
|
||||||
// and not if we are in admin sidebar
|
this.sidebarState.setPanel(MAIN_PANEL);
|
||||||
if (this.sidebarState.isForcingSidebar && !isAdminSidebarActive) {
|
|
||||||
this.sidebarState.setPanel(MAIN_PANEL); // Return to main sidebar panel
|
|
||||||
this.sidebarState.isForcingSidebar = false;
|
this.sidebarState.isForcingSidebar = false;
|
||||||
this.appEvents.trigger("discourse-ai:stop-forcing-conversations-sidebar");
|
this.appEvents.trigger("discourse-ai:stop-forcing-conversations-sidebar");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._removeScrollListener();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchMessages() {
|
||||||
|
if (this.isFetching || !this.hasMore) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isFirstPage = this.page === 0;
|
||||||
|
this.isFetching = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let { conversations, meta } = await ajax(
|
||||||
|
"/discourse-ai/ai-bot/conversations.json",
|
||||||
|
{ data: { page: this.page, per_page: 40 } }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isFirstPage) {
|
||||||
|
this.topics = conversations;
|
||||||
|
} else {
|
||||||
|
this.topics = [...this.topics, ...conversations];
|
||||||
|
// force rerender when fetching more messages
|
||||||
|
this.sidebarState.setPanel(AI_CONVERSATIONS_PANEL);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.page += 1;
|
||||||
|
this.hasMore = meta.has_more;
|
||||||
|
|
||||||
|
this._rebuildSections();
|
||||||
|
} finally {
|
||||||
|
this.isFetching = false;
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleNewBotPM(topic) {
|
||||||
|
this.topics = [topic, ...this.topics];
|
||||||
|
this._rebuildSections();
|
||||||
|
this._watchForTitleUpdate(topic.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
_watchForTitleUpdate(topicId) {
|
||||||
|
if (this._subscribedTopicIds?.has(topicId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._subscribedTopicIds = this._subscribedTopicIds || new Set();
|
||||||
|
this._subscribedTopicIds.add(topicId);
|
||||||
|
|
||||||
|
const channel = `/discourse-ai/ai-bot/topic/${topicId}`;
|
||||||
|
|
||||||
|
this.messageBus.subscribe(channel, (payload) => {
|
||||||
|
this._applyTitleUpdate(topicId, payload.title);
|
||||||
|
this.messageBus.unsubscribe(channel);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_applyTitleUpdate(topicId, newTitle) {
|
||||||
|
this.topics = this.topics.map((t) =>
|
||||||
|
t.id === topicId ? { ...t, title: newTitle } : t
|
||||||
|
);
|
||||||
|
|
||||||
|
this._rebuildSections();
|
||||||
|
}
|
||||||
|
|
||||||
|
// organize by date and create a section for each date group
|
||||||
|
_rebuildSections() {
|
||||||
|
const now = Date.now();
|
||||||
|
const fresh = [];
|
||||||
|
|
||||||
|
this.topics.forEach((t) => {
|
||||||
|
const postedAtMs = new Date(t.last_posted_at || now).valueOf();
|
||||||
|
const diffDays = Math.floor((now - postedAtMs) / 86400000);
|
||||||
|
let dateGroup;
|
||||||
|
|
||||||
|
if (diffDays <= 1) {
|
||||||
|
dateGroup = "today";
|
||||||
|
} else if (diffDays <= 7) {
|
||||||
|
dateGroup = "last-7-days";
|
||||||
|
} else if (diffDays <= 30) {
|
||||||
|
dateGroup = "last-30-days";
|
||||||
|
} else {
|
||||||
|
const d = new Date(postedAtMs);
|
||||||
|
const key = `${d.getFullYear()}-${d.getMonth()}`;
|
||||||
|
dateGroup = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
let sec = fresh.find((s) => s.name === dateGroup);
|
||||||
|
if (!sec) {
|
||||||
|
let title;
|
||||||
|
switch (dateGroup) {
|
||||||
|
case "today":
|
||||||
|
title = i18n("discourse_ai.ai_bot.conversations.today");
|
||||||
|
break;
|
||||||
|
case "last-7-days":
|
||||||
|
title = i18n("discourse_ai.ai_bot.conversations.last_7_days");
|
||||||
|
break;
|
||||||
|
case "last-30-days":
|
||||||
|
title = i18n("discourse_ai.ai_bot.conversations.last_30_days");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
title = autoUpdatingRelativeAge(new Date(t.last_posted_at));
|
||||||
|
}
|
||||||
|
sec = { name: dateGroup, title, links: new TrackedArray() };
|
||||||
|
fresh.push(sec);
|
||||||
|
}
|
||||||
|
|
||||||
|
sec.links.push({
|
||||||
|
key: t.id,
|
||||||
|
route: "topic.fromParamsNear",
|
||||||
|
models: [t.slug, t.id, t.last_read_post_number || 0],
|
||||||
|
title: t.title,
|
||||||
|
text: t.title,
|
||||||
|
classNames: `ai-conversation-${t.id}`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.sections = new TrackedArray(fresh);
|
||||||
|
|
||||||
|
// register each new section once
|
||||||
|
for (let sec of fresh) {
|
||||||
|
if (this._registered.has(sec.name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
this._registered.add(sec.name);
|
||||||
|
|
||||||
|
this.api.addSidebarSection((BaseCustomSidebarSection) => {
|
||||||
|
return class extends BaseCustomSidebarSection {
|
||||||
|
@service("ai-conversations-sidebar-manager") manager;
|
||||||
|
@service("appEvents") events;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
scheduleOnce("afterRender", this, this.triggerEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
triggerEvent() {
|
||||||
|
this.events.trigger("discourse-ai:conversations-sidebar-updated");
|
||||||
|
}
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return sec.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
get title() {
|
||||||
|
return sec.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
get text() {
|
||||||
|
return htmlSafe(sec.title);
|
||||||
|
}
|
||||||
|
|
||||||
|
get links() {
|
||||||
|
return (
|
||||||
|
this.manager.sections.find((s) => s.name === sec.name)?.links ||
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get emptyStateComponent() {
|
||||||
|
if (!this.manager.isLoading && this.links.length === 0) {
|
||||||
|
return AiBotSidebarEmptyState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, AI_CONVERSATIONS_PANEL);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,4 @@
|
|||||||
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 { 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 AiBotSidebarNewConversation from "../discourse/components/ai-bot-sidebar-new-conversation";
|
||||||
import { AI_CONVERSATIONS_PANEL } from "../discourse/services/ai-conversations-sidebar-manager";
|
import { AI_CONVERSATIONS_PANEL } from "../discourse/services/ai-conversations-sidebar-manager";
|
||||||
|
|
||||||
@ -28,8 +20,7 @@ export default {
|
|||||||
const aiConversationsSidebarManager = api.container.lookup(
|
const aiConversationsSidebarManager = api.container.lookup(
|
||||||
"service:ai-conversations-sidebar-manager"
|
"service:ai-conversations-sidebar-manager"
|
||||||
);
|
);
|
||||||
const appEvents = api.container.lookup("service:app-events");
|
aiConversationsSidebarManager.api = api;
|
||||||
const messageBus = api.container.lookup("service:message-bus");
|
|
||||||
|
|
||||||
api.addSidebarPanel(
|
api.addSidebarPanel(
|
||||||
(BaseCustomSidebarPanel) =>
|
(BaseCustomSidebarPanel) =>
|
||||||
@ -45,293 +36,6 @@ export default {
|
|||||||
"before-sidebar-sections",
|
"before-sidebar-sections",
|
||||||
AiBotSidebarNewConversation
|
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) => {
|
const setSidebarPanel = (transition) => {
|
||||||
if (transition?.to?.name === "discourse-ai-bot-conversations") {
|
if (transition?.to?.name === "discourse-ai-bot-conversations") {
|
||||||
@ -349,15 +53,6 @@ export default {
|
|||||||
return aiConversationsSidebarManager.forceCustomSidebar();
|
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();
|
aiConversationsSidebarManager.stopForcingCustomSidebar();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -27,48 +27,6 @@ body.has-ai-conversations-sidebar {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-wrapper,
|
|
||||||
.hamburger-dropdown-wrapper {
|
|
||||||
// ai related sidebar content
|
|
||||||
[data-section-name="ai-conversations-history"] {
|
|
||||||
.sidebar-section-header-wrapper {
|
|
||||||
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;
|
|
||||||
font-size: var(--font-down-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-section-link {
|
|
||||||
height: unset;
|
|
||||||
padding-block: 0.65em;
|
|
||||||
font-size: var(--font-down-1);
|
|
||||||
letter-spacing: 0.35px;
|
|
||||||
border-radius: 0 var(--border-radius) var(--border-radius) 0;
|
|
||||||
|
|
||||||
.sidebar-section-link-content-text {
|
|
||||||
white-space: normal;
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-section-link-prefix {
|
|
||||||
align-self: start;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// topic elements
|
// topic elements
|
||||||
#topic-footer-button-share-and-invite,
|
#topic-footer-button-share-and-invite,
|
||||||
body:not(.staff) #topic-footer-button-archive,
|
body:not(.staff) #topic-footer-button-archive,
|
||||||
|
@ -222,8 +222,7 @@ RSpec.describe "AI Bot - Homepage", type: :system do
|
|||||||
header.click_bot_button
|
header.click_bot_button
|
||||||
|
|
||||||
expect(ai_pm_homepage).to have_homepage
|
expect(ai_pm_homepage).to have_homepage
|
||||||
expect(sidebar).to have_section("ai-conversations-history")
|
expect(sidebar).to have_section("Today")
|
||||||
expect(sidebar).to have_section_link("Today")
|
|
||||||
expect(sidebar).to have_section_link(pm.title)
|
expect(sidebar).to have_section_link(pm.title)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -233,7 +232,7 @@ RSpec.describe "AI Bot - Homepage", type: :system do
|
|||||||
header.click_bot_button
|
header.click_bot_button
|
||||||
|
|
||||||
expect(ai_pm_homepage).to have_homepage
|
expect(ai_pm_homepage).to have_homepage
|
||||||
expect(sidebar).to have_section_link("Last 7 days")
|
expect(sidebar).to have_section("Last 7 days")
|
||||||
end
|
end
|
||||||
|
|
||||||
it "displays last_30_days label in the sidebar" do
|
it "displays last_30_days label in the sidebar" do
|
||||||
@ -242,7 +241,7 @@ RSpec.describe "AI Bot - Homepage", type: :system do
|
|||||||
header.click_bot_button
|
header.click_bot_button
|
||||||
|
|
||||||
expect(ai_pm_homepage).to have_homepage
|
expect(ai_pm_homepage).to have_homepage
|
||||||
expect(sidebar).to have_section_link("Last 30 days")
|
expect(sidebar).to have_section("Last 30 days")
|
||||||
end
|
end
|
||||||
|
|
||||||
it "displays month and year label in the sidebar for older conversations" do
|
it "displays month and year label in the sidebar for older conversations" do
|
||||||
@ -251,7 +250,7 @@ RSpec.describe "AI Bot - Homepage", type: :system do
|
|||||||
header.click_bot_button
|
header.click_bot_button
|
||||||
|
|
||||||
expect(ai_pm_homepage).to have_homepage
|
expect(ai_pm_homepage).to have_homepage
|
||||||
expect(sidebar).to have_section_link("Apr 2024")
|
expect(sidebar).to have_section("2024-3")
|
||||||
end
|
end
|
||||||
|
|
||||||
it "navigates to the bot conversation when clicked" do
|
it "navigates to the bot conversation when clicked" do
|
||||||
@ -328,12 +327,6 @@ RSpec.describe "AI Bot - Homepage", type: :system do
|
|||||||
expect(sidebar).to have_no_section_link(pm.title)
|
expect(sidebar).to have_no_section_link(pm.title)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "renders empty state in sidebar with no bot PM history" do
|
|
||||||
sign_in(user_2)
|
|
||||||
ai_pm_homepage.visit
|
|
||||||
expect(ai_pm_homepage).to have_empty_state
|
|
||||||
end
|
|
||||||
|
|
||||||
it "Allows choosing persona and LLM" do
|
it "Allows choosing persona and LLM" do
|
||||||
ai_pm_homepage.visit
|
ai_pm_homepage.visit
|
||||||
|
|
||||||
|
@ -52,13 +52,9 @@ module PageObjects
|
|||||||
page.find(".ai-new-question-button").click
|
page.find(".ai-new-question-button").click
|
||||||
end
|
end
|
||||||
|
|
||||||
def has_empty_state?
|
|
||||||
page.has_css?(".ai-bot-sidebar-empty-state")
|
|
||||||
end
|
|
||||||
|
|
||||||
def click_fist_sidebar_conversation
|
def click_fist_sidebar_conversation
|
||||||
page.find(
|
page.find(
|
||||||
".sidebar-section[data-section-name='ai-conversations-history'] a.sidebar-section-link:not(.date-heading)",
|
".sidebar-section-content a.sidebar-section-link",
|
||||||
).click
|
).click
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user