mirror of
https://ghfast.top/https://github.com/discourse/discourse-ai.git
synced 2026-07-16 11:36:23 +08:00
The Today section was added always, but a side-effect was that we hid the empty state component. This commit brings back the empty state
339 lines
8.7 KiB
JavaScript
339 lines
8.7 KiB
JavaScript
import { tracked } from "@glimmer/tracking";
|
|
import { scheduleOnce } from "@ember/runloop";
|
|
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 { i18n } from "discourse-i18n";
|
|
import AiBotSidebarEmptyState from "../../discourse/components/ai-bot-sidebar-empty-state";
|
|
|
|
export const AI_CONVERSATIONS_PANEL = "ai-conversations";
|
|
const SCROLL_BUFFER = 100;
|
|
const DEBOUNCE = 100;
|
|
|
|
export default class AiConversationsSidebarManager extends Service {
|
|
@service appEvents;
|
|
@service sidebarState;
|
|
@service messageBus;
|
|
|
|
@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
|
|
);
|
|
}
|
|
|
|
addEmptyStateClass() {
|
|
document.body.classList.toggle(
|
|
"has-empty-ai-conversations-sidebar",
|
|
!this.topics.length
|
|
);
|
|
}
|
|
|
|
forceCustomSidebar() {
|
|
document.body.classList.add("has-ai-conversations-sidebar");
|
|
if (!this.sidebarState.isForcingSidebar) {
|
|
this.appEvents.trigger("discourse-ai:force-conversations-sidebar");
|
|
}
|
|
|
|
this.sidebarState.isForcingSidebar = true;
|
|
|
|
// calling this before fetching data
|
|
// helps avoid flash of main sidebar mode
|
|
this.sidebarState.setPanel(AI_CONVERSATIONS_PANEL);
|
|
this.sidebarState.setSeparatedMode();
|
|
this.sidebarState.hideSwitchPanelButtons();
|
|
|
|
// don't render sidebar multiple times
|
|
if (this._didInit) {
|
|
this.addEmptyStateClass();
|
|
return true;
|
|
}
|
|
|
|
this._didInit = true;
|
|
|
|
this.fetchMessages().then(() => {
|
|
this.sidebarState.setPanel(AI_CONVERSATIONS_PANEL);
|
|
this.addEmptyStateClass();
|
|
});
|
|
|
|
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() {
|
|
document.body.classList.remove("has-ai-conversations-sidebar");
|
|
document.body.classList.remove("has-empty-ai-conversations-sidebar");
|
|
|
|
const isAdmin = this.sidebarState.currentPanel?.key === ADMIN_PANEL;
|
|
if (this.sidebarState.isForcingSidebar && !isAdmin) {
|
|
this.sidebarState.setPanel(MAIN_PANEL);
|
|
this.sidebarState.isForcingSidebar = false;
|
|
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 = [];
|
|
|
|
const todaySection = {
|
|
name: "today",
|
|
title: i18n("discourse_ai.ai_bot.conversations.today"),
|
|
links: new TrackedArray(),
|
|
};
|
|
fresh.push(todaySection);
|
|
|
|
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;
|
|
if (dateGroup === "today") {
|
|
sec = todaySection;
|
|
} else {
|
|
sec = fresh.find((s) => s.name === dateGroup);
|
|
}
|
|
|
|
if (!sec) {
|
|
let title;
|
|
switch (dateGroup) {
|
|
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);
|
|
}
|
|
}
|
|
}
|