mirror of
https://github.com/discourse/discourse-ai.git
synced 2025-07-07 15:02:25 +00:00
FEATURE: Bot Conversation Homepage
This commit is contained in:
parent
d26c7ac48d
commit
3775f4dfe0
@ -0,0 +1,35 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module DiscourseAi
|
||||||
|
module AiBot
|
||||||
|
class ConversationsController < ::ApplicationController
|
||||||
|
requires_plugin ::DiscourseAi::PLUGIN_NAME
|
||||||
|
requires_login
|
||||||
|
|
||||||
|
def index
|
||||||
|
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)
|
||||||
|
.joins(:topic_users)
|
||||||
|
.where(topic_users: { user_id: bot_user_ids })
|
||||||
|
.distinct
|
||||||
|
total = base_query.count
|
||||||
|
pms = base_query.order(last_posted_at: :desc).offset(page * per_page).limit(per_page)
|
||||||
|
|
||||||
|
render json: {
|
||||||
|
conversations: serialize_data(pms, BasicTopicSerializer),
|
||||||
|
meta: {
|
||||||
|
total: total,
|
||||||
|
page: page,
|
||||||
|
per_page: per_page,
|
||||||
|
more: total > (page + 1) * per_page,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -9,6 +9,7 @@ export default class AiBotHeaderIcon extends Component {
|
|||||||
@service currentUser;
|
@service currentUser;
|
||||||
@service siteSettings;
|
@service siteSettings;
|
||||||
@service composer;
|
@service composer;
|
||||||
|
@service router;
|
||||||
|
|
||||||
get bots() {
|
get bots() {
|
||||||
const availableBots = this.currentUser.ai_enabled_chat_bots
|
const availableBots = this.currentUser.ai_enabled_chat_bots
|
||||||
@ -24,6 +25,9 @@ export default class AiBotHeaderIcon extends Component {
|
|||||||
|
|
||||||
@action
|
@action
|
||||||
compose() {
|
compose() {
|
||||||
|
if (this.siteSettings.ai_enable_experimental_bot_ux) {
|
||||||
|
return this.router.transitionTo("discourse-ai-bot-conversations");
|
||||||
|
}
|
||||||
composeAiBotMessage(this.bots[0], this.composer);
|
composeAiBotMessage(this.bots[0], this.composer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,27 @@
|
|||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { service } from "@ember/service";
|
||||||
|
import DButton from "discourse/components/d-button";
|
||||||
|
import { AI_CONVERSATIONS_PANEL } from "../services/ai-conversations-sidebar-manager";
|
||||||
|
|
||||||
|
export default class AiBotSidebarNewConversation extends Component {
|
||||||
|
@service router;
|
||||||
|
@service sidebarState;
|
||||||
|
|
||||||
|
get shouldRender() {
|
||||||
|
return (
|
||||||
|
this.router.currentRouteName !== "discourse-ai-bot-conversations" &&
|
||||||
|
this.sidebarState.isCurrentPanel(AI_CONVERSATIONS_PANEL)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
{{#if this.shouldRender}}
|
||||||
|
<DButton
|
||||||
|
@route="/discourse-ai/ai-bot/conversations"
|
||||||
|
@label="discourse_ai.ai_bot.conversations.new"
|
||||||
|
@icon="plus"
|
||||||
|
class="ai-new-question-button btn-default"
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
|
</template>
|
||||||
|
}
|
@ -0,0 +1,66 @@
|
|||||||
|
import Controller from "@ember/controller";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
import { service } from "@ember/service";
|
||||||
|
import { tracked } from "@ember-compat/tracked-built-ins";
|
||||||
|
|
||||||
|
export default class DiscourseAiBotConversations extends Controller {
|
||||||
|
@service aiBotConversationsHiddenSubmit;
|
||||||
|
@service currentUser;
|
||||||
|
|
||||||
|
@tracked selectedPersona = this.personaOptions[0].username;
|
||||||
|
|
||||||
|
textarea = null;
|
||||||
|
|
||||||
|
init() {
|
||||||
|
super.init(...arguments);
|
||||||
|
this.selectedPersonaChanged(this.selectedPersona);
|
||||||
|
}
|
||||||
|
|
||||||
|
get personaOptions() {
|
||||||
|
if (this.currentUser.ai_enabled_personas) {
|
||||||
|
return this.currentUser.ai_enabled_personas
|
||||||
|
.filter((persona) => persona.username)
|
||||||
|
.map((persona) => {
|
||||||
|
return {
|
||||||
|
id: persona.id,
|
||||||
|
username: persona.username,
|
||||||
|
name: persona.name,
|
||||||
|
description: persona.description,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get filterable() {
|
||||||
|
return this.personaOptions.length > 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
selectedPersonaChanged(username) {
|
||||||
|
this.selectedPersona = username;
|
||||||
|
this.aiBotConversationsHiddenSubmit.personaUsername = username;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
updateInputValue(event) {
|
||||||
|
this._autoExpandTextarea();
|
||||||
|
this.aiBotConversationsHiddenSubmit.inputValue = event.target.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
handleKeyDown(event) {
|
||||||
|
if (event.key === "Enter" && !event.shiftKey) {
|
||||||
|
this.aiBotConversationsHiddenSubmit.submitToBot();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
setTextArea(element) {
|
||||||
|
this.textarea = element;
|
||||||
|
}
|
||||||
|
|
||||||
|
_autoExpandTextarea() {
|
||||||
|
this.textarea.style.height = "auto";
|
||||||
|
this.textarea.style.height = this.textarea.scrollHeight + "px";
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
export default function () {
|
||||||
|
this.route("discourse-ai-bot-conversations", {
|
||||||
|
path: "/discourse-ai/ai-bot/conversations",
|
||||||
|
});
|
||||||
|
}
|
@ -23,17 +23,30 @@ export function showShareConversationModal(modal, topicId) {
|
|||||||
.catch(popupAjaxError);
|
.catch(popupAjaxError);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function composeAiBotMessage(targetBot, composer) {
|
export async function composeAiBotMessage(
|
||||||
|
targetBot,
|
||||||
|
composer,
|
||||||
|
options = {
|
||||||
|
skipFocus: false,
|
||||||
|
topicBody: "",
|
||||||
|
personaUsername: null,
|
||||||
|
}
|
||||||
|
) {
|
||||||
const currentUser = composer.currentUser;
|
const currentUser = composer.currentUser;
|
||||||
const draftKey = "new_private_message_ai_" + new Date().getTime();
|
const draftKey = "new_private_message_ai_" + new Date().getTime();
|
||||||
|
|
||||||
let botUsername = currentUser.ai_enabled_chat_bots.find(
|
let botUsername;
|
||||||
|
if (targetBot) {
|
||||||
|
botUsername = currentUser.ai_enabled_chat_bots.find(
|
||||||
(bot) => bot.model_name === targetBot
|
(bot) => bot.model_name === targetBot
|
||||||
).username;
|
)?.username;
|
||||||
|
} else if (options.personaUsername) {
|
||||||
|
botUsername = options.personaUsername;
|
||||||
|
} else {
|
||||||
|
botUsername = currentUser.ai_enabled_chat_bots[0].username;
|
||||||
|
}
|
||||||
|
|
||||||
composer.focusComposer({
|
const data = {
|
||||||
fallbackToNewTopic: true,
|
|
||||||
openOpts: {
|
|
||||||
action: Composer.PRIVATE_MESSAGE,
|
action: Composer.PRIVATE_MESSAGE,
|
||||||
recipients: botUsername,
|
recipients: botUsername,
|
||||||
topicTitle: i18n("discourse_ai.ai_bot.default_pm_prefix"),
|
topicTitle: i18n("discourse_ai.ai_bot.default_pm_prefix"),
|
||||||
@ -41,6 +54,12 @@ export function composeAiBotMessage(targetBot, composer) {
|
|||||||
draftKey,
|
draftKey,
|
||||||
hasGroups: false,
|
hasGroups: false,
|
||||||
warningsDisabled: true,
|
warningsDisabled: true,
|
||||||
},
|
};
|
||||||
});
|
|
||||||
|
if (options.skipFocus) {
|
||||||
|
data.topicBody = options.topicBody;
|
||||||
|
await composer.open(data);
|
||||||
|
} else {
|
||||||
|
composer.focusComposer({ fallbackToNewTopic: true, openOpts: data });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
import DiscourseRoute from "discourse/routes/discourse";
|
||||||
|
|
||||||
|
export default class DiscourseAiBotConversationsRoute extends DiscourseRoute {}
|
@ -0,0 +1,62 @@
|
|||||||
|
import { action } from "@ember/object";
|
||||||
|
import { next } from "@ember/runloop";
|
||||||
|
import Service, { service } from "@ember/service";
|
||||||
|
import { i18n } from "discourse-i18n";
|
||||||
|
import { composeAiBotMessage } from "../lib/ai-bot-helper";
|
||||||
|
|
||||||
|
export default class AiBotConversationsHiddenSubmit extends Service {
|
||||||
|
@service composer;
|
||||||
|
@service aiConversationsSidebarManager;
|
||||||
|
@service dialog;
|
||||||
|
|
||||||
|
personaUsername;
|
||||||
|
|
||||||
|
inputValue = "";
|
||||||
|
|
||||||
|
@action
|
||||||
|
focusInput() {
|
||||||
|
this.composer.destroyDraft();
|
||||||
|
this.composer.close();
|
||||||
|
next(() => {
|
||||||
|
document.getElementById("custom-homepage-input").focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
async submitToBot() {
|
||||||
|
this.composer.destroyDraft();
|
||||||
|
this.composer.close();
|
||||||
|
|
||||||
|
if (this.inputValue.length < 10) {
|
||||||
|
return this.dialog.alert({
|
||||||
|
message: i18n(
|
||||||
|
"discourse_ai.ai_bot.conversations.min_input_length_message"
|
||||||
|
),
|
||||||
|
didConfirm: () => this.focusInput(),
|
||||||
|
didCancel: () => this.focusInput(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// we are intentionally passing null as the targetBot to allow for the
|
||||||
|
// function to select the first available bot. This will be refactored in the
|
||||||
|
// future to allow for selecting a specific bot.
|
||||||
|
await composeAiBotMessage(null, this.composer, {
|
||||||
|
skipFocus: true,
|
||||||
|
topicBody: this.inputValue,
|
||||||
|
personaUsername: this.personaUsername,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.composer.save();
|
||||||
|
this.aiConversationsSidebarManager.newTopicForceSidebar = true;
|
||||||
|
if (this.inputValue.length > 10) {
|
||||||
|
// prevents submitting same message again when returning home
|
||||||
|
// but avoids deleting too-short message on submit
|
||||||
|
this.inputValue = "";
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error("Failed to submit message:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
import Service, { service } from "@ember/service";
|
||||||
|
import { ADMIN_PANEL, MAIN_PANEL } from "discourse/lib/sidebar/panels";
|
||||||
|
|
||||||
|
export const AI_CONVERSATIONS_PANEL = "ai-conversations";
|
||||||
|
|
||||||
|
export default class AiConversationsSidebarManager extends Service {
|
||||||
|
@service sidebarState;
|
||||||
|
|
||||||
|
@tracked newTopicForceSidebar = false;
|
||||||
|
|
||||||
|
forceCustomSidebar() {
|
||||||
|
// Set the panel to your custom panel
|
||||||
|
this.sidebarState.setPanel(AI_CONVERSATIONS_PANEL);
|
||||||
|
|
||||||
|
// Use separated mode to ensure independence from hamburger menu
|
||||||
|
this.sidebarState.setSeparatedMode();
|
||||||
|
|
||||||
|
// Hide panel switching buttons to keep UI clean
|
||||||
|
this.sidebarState.hideSwitchPanelButtons();
|
||||||
|
|
||||||
|
this.sidebarState.isForcingSidebar = true;
|
||||||
|
document.body.classList.add("has-ai-conversations-sidebar");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
const isAdminSidebarActive =
|
||||||
|
this.sidebarState.currentPanel?.key === ADMIN_PANEL;
|
||||||
|
// only restore main panel if we previously forced our sidebar
|
||||||
|
// and not if we are in admin sidebar
|
||||||
|
if (this.sidebarState.isForcingSidebar && !isAdminSidebarActive) {
|
||||||
|
this.sidebarState.setPanel(MAIN_PANEL); // Return to main sidebar panel
|
||||||
|
this.sidebarState.isForcingSidebar = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
<div class="ai-bot-conversations">
|
||||||
|
<div class="ai-bot-conversations__persona-selector">
|
||||||
|
<DropdownSelectBox
|
||||||
|
class="persona-llm-selector__persona-dropdown"
|
||||||
|
@value={{this.selectedPersona}}
|
||||||
|
@valueProperty="username"
|
||||||
|
@content={{this.personaOptions}}
|
||||||
|
@options={{hash icon="robot" filterable=this.filterable}}
|
||||||
|
@onChange={{this.selectedPersonaChanged}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ai-bot-conversations__content-wrapper">
|
||||||
|
|
||||||
|
<h1>{{i18n "discourse_ai.ai_bot.conversations.header"}}</h1>
|
||||||
|
<div class="ai-bot-conversations__input-wrapper">
|
||||||
|
<textarea
|
||||||
|
{{didInsert this.setTextArea}}
|
||||||
|
{{on "input" this.updateInputValue}}
|
||||||
|
{{on "keydown" this.handleKeyDown}}
|
||||||
|
id="ai-bot-conversations-input"
|
||||||
|
placeholder={{i18n "discourse_ai.ai_bot.conversations.placeholder"}}
|
||||||
|
minlength="10"
|
||||||
|
rows="1"
|
||||||
|
/>
|
||||||
|
<DButton
|
||||||
|
@action={{this.aiBotConversationsHiddenSubmit.submitToBot}}
|
||||||
|
@icon="paper-plane"
|
||||||
|
@title="discourse_ai.ai_bot.conversations.header"
|
||||||
|
class="ai-bot-button btn-primary ai-conversation-submit"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p class="ai-disclaimer">
|
||||||
|
{{i18n "discourse_ai.ai_bot.conversations.disclaimer"}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
256
assets/javascripts/initializers/ai-conversations-sidebar.js
Normal file
256
assets/javascripts/initializers/ai-conversations-sidebar.js
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
import { TrackedArray } from "@ember-compat/tracked-built-ins";
|
||||||
|
import { ajax } from "discourse/lib/ajax";
|
||||||
|
import { bind } from "discourse/lib/decorators";
|
||||||
|
import { withPluginApi } from "discourse/lib/plugin-api";
|
||||||
|
import { i18n } from "discourse-i18n";
|
||||||
|
import AiBotSidebarNewConversation from "../discourse/components/ai-bot-sidebar-new-conversation";
|
||||||
|
import { isPostFromAiBot } from "../discourse/lib/ai-bot-helper";
|
||||||
|
import { AI_CONVERSATIONS_PANEL } from "../discourse/services/ai-conversations-sidebar-manager";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "ai-conversations-sidebar",
|
||||||
|
|
||||||
|
initialize() {
|
||||||
|
withPluginApi("1.8.0", (api) => {
|
||||||
|
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");
|
||||||
|
|
||||||
|
api.addSidebarPanel(
|
||||||
|
(BaseCustomSidebarPanel) =>
|
||||||
|
class AiConversationsSidebarPanel extends BaseCustomSidebarPanel {
|
||||||
|
key = AI_CONVERSATIONS_PANEL;
|
||||||
|
hidden = true;
|
||||||
|
displayHeader = true;
|
||||||
|
expandActiveSection = true;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
api.renderInOutlet("sidebar-footer-actions", AiBotSidebarNewConversation);
|
||||||
|
api.addSidebarSection(
|
||||||
|
(BaseCustomSidebarSection, BaseCustomSidebarSectionLink) => {
|
||||||
|
const AiConversationLink = class extends BaseCustomSidebarSectionLink {
|
||||||
|
route = "topic.fromParamsNear";
|
||||||
|
|
||||||
|
constructor(topic) {
|
||||||
|
super(...arguments);
|
||||||
|
this.topic = topic;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = [];
|
||||||
|
page = 0;
|
||||||
|
isFetching = false;
|
||||||
|
totalTopicsCount = 0;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
this.fetchMessages();
|
||||||
|
|
||||||
|
appEvents.on("topic:created", this, "addNewMessageToSidebar");
|
||||||
|
}
|
||||||
|
|
||||||
|
@bind
|
||||||
|
willDestroy() {
|
||||||
|
this.removeScrollListener();
|
||||||
|
appEvents.on("topic:created", this, "addNewMessageToSidebar");
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
addNewMessageToSidebar(topic) {
|
||||||
|
this.addNewMessage(topic);
|
||||||
|
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.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
buildSidebarLinks() {
|
||||||
|
this.links = this.topics.map(
|
||||||
|
(topic) => new AiConversationLink(topic)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
addNewMessage(newTopic) {
|
||||||
|
this.links = [new AiConversationLink(newTopic), ...this.links];
|
||||||
|
}
|
||||||
|
|
||||||
|
watchForTitleUpdate(topic) {
|
||||||
|
const channel = `/discourse-ai/ai-bot/topic/${topic.topic_id}`;
|
||||||
|
const topicId = topic.topic_id;
|
||||||
|
const callback = this.updateTopicTitle.bind(this);
|
||||||
|
messageBus.subscribe(channel, ({ title }) => {
|
||||||
|
callback(topicId, title);
|
||||||
|
messageBus.unsubscribe(channel);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTopicTitle(topicId, title) {
|
||||||
|
// update the topic title in the sidebar, instead of the default title
|
||||||
|
const text = document.querySelector(
|
||||||
|
`.sidebar-section-link-wrapper .ai-conversation-${topicId} .sidebar-section-link-content-text`
|
||||||
|
);
|
||||||
|
if (text) {
|
||||||
|
text.innerText = title;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
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 (
|
||||||
|
topic?.archetype === "private_message" &&
|
||||||
|
topic.postStream.posts.some((post) =>
|
||||||
|
isPostFromAiBot(post, currentUser)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
341
assets/stylesheets/modules/ai-bot-conversations/common.scss
Normal file
341
assets/stylesheets/modules/ai-bot-conversations/common.scss
Normal file
@ -0,0 +1,341 @@
|
|||||||
|
// Hide the new question button from the hamburger menu's footer
|
||||||
|
.hamburger-panel .ai-new-question-button {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.has-ai-conversations-sidebar {
|
||||||
|
.sidebar-wrapper {
|
||||||
|
.sidebar-footer-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-left: 0;
|
||||||
|
align-items: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.ai-new-question-button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-container {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-section-link-wrapper {
|
||||||
|
.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-footer-button-share-and-invite,
|
||||||
|
body:not(.staff) #topic-footer-button-archive,
|
||||||
|
#topic-footer-buttons .topic-notifications-button,
|
||||||
|
.bookmark-menu-trigger,
|
||||||
|
.more-topics__container,
|
||||||
|
.private-message-glyph-wrapper,
|
||||||
|
.topic-header-participants,
|
||||||
|
.topic-above-footer-buttons-outlet,
|
||||||
|
.topic-map,
|
||||||
|
.timeline-ago,
|
||||||
|
#topic-footer-buttons .topic-footer-main-buttons details {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topic-timer-info {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topic-owner .actions .create-flag {
|
||||||
|
// why flag my own post
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container.posts {
|
||||||
|
margin-bottom: 0;
|
||||||
|
|
||||||
|
.topic-navigation.with-timeline {
|
||||||
|
top: calc(var(--header-offset, 60px) + 5.5em);
|
||||||
|
}
|
||||||
|
|
||||||
|
.topic-navigation {
|
||||||
|
.topic-notifications-button {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#topic-title {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.title-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 960px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.small-action,
|
||||||
|
.onscreen-post .row {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#topic-footer-buttons {
|
||||||
|
width: calc(100% - 6.5em);
|
||||||
|
margin-top: 0;
|
||||||
|
|
||||||
|
@media screen and (max-width: 924px) {
|
||||||
|
max-width: unset;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 1300px) {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 51em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topic-footer-main-buttons {
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
@media screen and (min-width: 1180px) {
|
||||||
|
margin-right: 0.6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 924px) {
|
||||||
|
margin-right: 0.6em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#topic-progress-wrapper.docked {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 924px) {
|
||||||
|
.archetype-private_message .topic-post:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nav.post-controls .actions button {
|
||||||
|
padding: 0.5em 0.65em;
|
||||||
|
|
||||||
|
&.reply {
|
||||||
|
.d-icon {
|
||||||
|
margin-right: 0.45em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.topic-footer-main-buttons {
|
||||||
|
margin-left: calc(var(--topic-avatar-width) - 1.15em);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-bot-conversations {
|
||||||
|
height: calc(100dvh - var(--header-offset) - 1.25em);
|
||||||
|
|
||||||
|
@media screen and (min-width: 675px) {
|
||||||
|
border: 1px solid var(--primary-low);
|
||||||
|
padding: 2em 2em 3em;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
height: calc(100dvh - var(--header-offset) - 10em);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__persona-selector {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-sizing: border-box;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__input-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 0.5em;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 90dvw;
|
||||||
|
margin-top: 2em;
|
||||||
|
|
||||||
|
@media screen and (min-width: 600px) {
|
||||||
|
width: 80%;
|
||||||
|
max-width: 46em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
align-self: end;
|
||||||
|
min-height: 2.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ai-bot-conversations-input {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
resize: none;
|
||||||
|
border-radius: var(--d-button-border-radius);
|
||||||
|
max-height: 30vh;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary-high);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-bottom: 0.45em;
|
||||||
|
max-width: 20em;
|
||||||
|
text-align: center;
|
||||||
|
font-size: var(--font-up-6);
|
||||||
|
line-height: var(--line-height-medium);
|
||||||
|
|
||||||
|
@media screen and (min-height: 300px) {
|
||||||
|
margin-top: -1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-height: 600px) {
|
||||||
|
margin-top: -3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-height: 900px) {
|
||||||
|
margin-top: -6em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-question-button {
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
border: 1px solid var(--primary-low);
|
||||||
|
line-height: var(--line-height-medium);
|
||||||
|
|
||||||
|
.d-button-label {
|
||||||
|
color: var(--primary-high);
|
||||||
|
}
|
||||||
|
|
||||||
|
.discourse-no-touch & {
|
||||||
|
&:hover {
|
||||||
|
background: var(--primary-low);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-disclaimer {
|
||||||
|
text-align: center;
|
||||||
|
font-size: var(--font-down-1);
|
||||||
|
color: var(--primary-700);
|
||||||
|
|
||||||
|
@media screen and (min-width: 600px) {
|
||||||
|
width: 80%;
|
||||||
|
max-width: 46em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-footer-wrapper {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.powered-by-discourse {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// composer
|
||||||
|
.reply-details .dropdown-select-box.composer-actions,
|
||||||
|
.composer-fields {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// hide user stuff
|
||||||
|
.new-user-wrapper {
|
||||||
|
.user-navigation {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-main .about.collapsed-info .details {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-content {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 600px) {
|
||||||
|
.share-ai-conversation-button {
|
||||||
|
.d-icon {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.d-button-label {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-view {
|
||||||
|
nav.post-controls .actions button.reply .d-icon {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-dropdown {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-custom-sections {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// custom user card link
|
||||||
|
.user-card-meta__profile-link {
|
||||||
|
display: block;
|
||||||
|
padding: 0.5em 0 0.25em;
|
||||||
|
|
||||||
|
.d-icon {
|
||||||
|
font-size: var(--font-down-1);
|
||||||
|
margin-right: 0.15em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -716,6 +716,14 @@ en:
|
|||||||
5-pro: "Gemini"
|
5-pro: "Gemini"
|
||||||
mixtral-8x7B-Instruct-V0:
|
mixtral-8x7B-Instruct-V0:
|
||||||
"1": "Mixtral-8x7B V0.1"
|
"1": "Mixtral-8x7B V0.1"
|
||||||
|
conversations:
|
||||||
|
header: "What can I help with?"
|
||||||
|
submit: "Submit question"
|
||||||
|
disclaimer: "Generative AI can make mistakes"
|
||||||
|
placeholder: "Ask a question..."
|
||||||
|
new: "New Question"
|
||||||
|
min_input_length_message: "Message must be longer than 10 characters"
|
||||||
|
messages_sidebar_title: "Conversations"
|
||||||
sentiments:
|
sentiments:
|
||||||
dashboard:
|
dashboard:
|
||||||
title: "Sentiment"
|
title: "Sentiment"
|
||||||
|
@ -37,6 +37,10 @@ DiscourseAi::Engine.routes.draw do
|
|||||||
get "/preview/:topic_id" => "shared_ai_conversations#preview"
|
get "/preview/:topic_id" => "shared_ai_conversations#preview"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
scope module: :ai_bot, path: "/ai-bot/conversations" do
|
||||||
|
get "/" => "conversations#index"
|
||||||
|
end
|
||||||
|
|
||||||
scope module: :ai_bot, path: "/ai-bot/artifacts" do
|
scope module: :ai_bot, path: "/ai-bot/artifacts" do
|
||||||
get "/:id" => "artifacts#show"
|
get "/:id" => "artifacts#show"
|
||||||
get "/:id/:version" => "artifacts#show"
|
get "/:id/:version" => "artifacts#show"
|
||||||
|
@ -390,3 +390,7 @@ discourse_ai:
|
|||||||
ai_rag_images_enabled:
|
ai_rag_images_enabled:
|
||||||
default: false
|
default: false
|
||||||
hidden: true
|
hidden: true
|
||||||
|
ai_enable_experimental_bot_ux:
|
||||||
|
default: false
|
||||||
|
client: true
|
||||||
|
|
||||||
|
@ -43,6 +43,8 @@ register_asset "stylesheets/modules/ai-bot/common/ai-persona.scss"
|
|||||||
register_asset "stylesheets/modules/ai-bot/common/ai-discobot-discoveries.scss"
|
register_asset "stylesheets/modules/ai-bot/common/ai-discobot-discoveries.scss"
|
||||||
register_asset "stylesheets/modules/ai-bot/mobile/ai-persona.scss", :mobile
|
register_asset "stylesheets/modules/ai-bot/mobile/ai-persona.scss", :mobile
|
||||||
|
|
||||||
|
register_asset "stylesheets/modules/ai-bot-conversations/common.scss"
|
||||||
|
|
||||||
register_asset "stylesheets/modules/embeddings/common/semantic-related-topics.scss"
|
register_asset "stylesheets/modules/embeddings/common/semantic-related-topics.scss"
|
||||||
register_asset "stylesheets/modules/embeddings/common/semantic-search.scss"
|
register_asset "stylesheets/modules/embeddings/common/semantic-search.scss"
|
||||||
|
|
||||||
|
185
spec/system/ai_bot/personal_message_spec.rb
Normal file
185
spec/system/ai_bot/personal_message_spec.rb
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
RSpec.describe "AI Bot - Personal Message", type: :system do
|
||||||
|
let(:topic_page) { PageObjects::Pages::Topic.new }
|
||||||
|
let(:composer) { PageObjects::Components::Composer.new }
|
||||||
|
let(:ai_pm_homepage) { PageObjects::Components::AiPmHomepage.new }
|
||||||
|
let(:sidebar) { PageObjects::Components::NavigationMenu::Sidebar.new }
|
||||||
|
let(:header_dropdown) { PageObjects::Components::NavigationMenu::HeaderDropdown.new }
|
||||||
|
let(:dialog) { PageObjects::Components::Dialog.new }
|
||||||
|
|
||||||
|
fab!(:user) { Fabricate(:user, refresh_auto_groups: true) }
|
||||||
|
|
||||||
|
fab!(:claude_2) do
|
||||||
|
Fabricate(
|
||||||
|
:llm_model,
|
||||||
|
provider: "anthropic",
|
||||||
|
url: "https://api.anthropic.com/v1/messages",
|
||||||
|
name: "claude-2",
|
||||||
|
)
|
||||||
|
end
|
||||||
|
fab!(:bot_user) do
|
||||||
|
toggle_enabled_bots(bots: [claude_2])
|
||||||
|
SiteSetting.ai_bot_enabled = true
|
||||||
|
claude_2.reload.user
|
||||||
|
end
|
||||||
|
fab!(:bot) do
|
||||||
|
persona =
|
||||||
|
AiPersona
|
||||||
|
.find(DiscourseAi::Personas::Persona.system_personas[DiscourseAi::Personas::General])
|
||||||
|
.class_instance
|
||||||
|
.new
|
||||||
|
DiscourseAi::Personas::Bot.as(bot_user, persona: persona)
|
||||||
|
end
|
||||||
|
|
||||||
|
fab!(:pm) do
|
||||||
|
Fabricate(
|
||||||
|
:private_message_topic,
|
||||||
|
title: "This is my special PM",
|
||||||
|
user: user,
|
||||||
|
topic_allowed_users: [
|
||||||
|
Fabricate.build(:topic_allowed_user, user: user),
|
||||||
|
Fabricate.build(:topic_allowed_user, user: bot_user),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
end
|
||||||
|
fab!(:first_post) do
|
||||||
|
Fabricate(:post, topic: pm, user: user, post_number: 1, raw: "This is a reply by the user")
|
||||||
|
end
|
||||||
|
fab!(:second_post) do
|
||||||
|
Fabricate(:post, topic: pm, user: bot_user, post_number: 2, raw: "This is a bot reply")
|
||||||
|
end
|
||||||
|
fab!(:third_post) do
|
||||||
|
Fabricate(
|
||||||
|
:post,
|
||||||
|
topic: pm,
|
||||||
|
user: user,
|
||||||
|
post_number: 3,
|
||||||
|
raw: "This is a second reply by the user",
|
||||||
|
)
|
||||||
|
end
|
||||||
|
fab!(:topic_user) { Fabricate(:topic_user, topic: pm, user: user) }
|
||||||
|
fab!(:topic_bot_user) { Fabricate(:topic_user, topic: pm, user: bot_user) }
|
||||||
|
|
||||||
|
fab!(:persona) do
|
||||||
|
persona =
|
||||||
|
AiPersona.create!(
|
||||||
|
name: "Test Persona",
|
||||||
|
description: "A test persona",
|
||||||
|
allowed_group_ids: [Group::AUTO_GROUPS[:trust_level_0]],
|
||||||
|
enabled: true,
|
||||||
|
system_prompt: "You are a helpful bot",
|
||||||
|
)
|
||||||
|
|
||||||
|
persona.create_user!
|
||||||
|
persona.update!(
|
||||||
|
default_llm_id: claude_2.id,
|
||||||
|
allow_chat_channel_mentions: true,
|
||||||
|
allow_topic_mentions: true,
|
||||||
|
)
|
||||||
|
persona
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
SiteSetting.ai_enable_experimental_bot_ux = true
|
||||||
|
SiteSetting.ai_bot_enabled = true
|
||||||
|
Jobs.run_immediately!
|
||||||
|
SiteSetting.ai_bot_allowed_groups = "#{Group::AUTO_GROUPS[:trust_level_0]}"
|
||||||
|
sign_in(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "has normal bot interaction when `ai_enable_experimental_bot_ux` is disabled" do
|
||||||
|
SiteSetting.ai_enable_experimental_bot_ux = false
|
||||||
|
visit "/"
|
||||||
|
find(".ai-bot-button").click
|
||||||
|
|
||||||
|
expect(ai_pm_homepage).to have_no_homepage
|
||||||
|
expect(composer).to be_opened
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when `ai_enable_experimental_bot_ux` is enabled" do
|
||||||
|
it "renders landing page on bot click" do
|
||||||
|
visit "/"
|
||||||
|
find(".ai-bot-button").click
|
||||||
|
expect(ai_pm_homepage).to have_homepage
|
||||||
|
expect(sidebar).to be_visible
|
||||||
|
end
|
||||||
|
|
||||||
|
it "displays error when message is too short" do
|
||||||
|
visit "/"
|
||||||
|
find(".ai-bot-button").click
|
||||||
|
|
||||||
|
ai_pm_homepage.input.fill_in(with: "a")
|
||||||
|
ai_pm_homepage.submit
|
||||||
|
expect(ai_pm_homepage).to have_too_short_dialog
|
||||||
|
dialog.click_yes
|
||||||
|
expect(composer).to be_closed
|
||||||
|
end
|
||||||
|
|
||||||
|
it "renders sidebar even when navigation menu is set to header" do
|
||||||
|
SiteSetting.navigation_menu = "header dropdown"
|
||||||
|
visit "/"
|
||||||
|
find(".ai-bot-button").click
|
||||||
|
expect(ai_pm_homepage).to have_homepage
|
||||||
|
expect(sidebar).to be_visible
|
||||||
|
expect(header_dropdown).to be_visible
|
||||||
|
end
|
||||||
|
|
||||||
|
it "hides default content in the sidebar" do
|
||||||
|
visit "/"
|
||||||
|
find(".ai-bot-button").click
|
||||||
|
|
||||||
|
expect(ai_pm_homepage).to have_homepage
|
||||||
|
expect(sidebar).to have_no_tags_section
|
||||||
|
expect(sidebar).to have_no_section("categories")
|
||||||
|
expect(sidebar).to have_no_section("messages")
|
||||||
|
expect(sidebar).to have_no_section("chat-dms")
|
||||||
|
expect(sidebar).to have_no_section("chat-channels")
|
||||||
|
expect(sidebar).to have_no_section("user-threads")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "shows the bot conversation in the sidebar" do
|
||||||
|
visit "/"
|
||||||
|
find(".ai-bot-button").click
|
||||||
|
|
||||||
|
expect(ai_pm_homepage).to have_homepage
|
||||||
|
expect(sidebar).to have_section("ai-conversations-history")
|
||||||
|
expect(sidebar).to have_section_link(pm.title)
|
||||||
|
expect(sidebar).to have_no_css("button.ai-new-question-button")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "navigates to the bot conversation when clicked" do
|
||||||
|
visit "/"
|
||||||
|
find(".ai-bot-button").click
|
||||||
|
|
||||||
|
expect(ai_pm_homepage).to have_homepage
|
||||||
|
sidebar.find(
|
||||||
|
".sidebar-section[data-section-name='ai-conversations-history'] a.sidebar-section-link",
|
||||||
|
).click
|
||||||
|
expect(topic_page).to have_topic_title(pm.title)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "displays sidebar and 'new question' on the topic page" do
|
||||||
|
topic_page.visit_topic(pm)
|
||||||
|
expect(sidebar).to be_visible
|
||||||
|
expect(sidebar).to have_css("button.ai-new-question-button")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "redirect to the homepage when 'new question' is clicked" do
|
||||||
|
topic_page.visit_topic(pm)
|
||||||
|
expect(sidebar).to be_visible
|
||||||
|
sidebar.find("button.ai-new-question-button").click
|
||||||
|
expect(ai_pm_homepage).to have_homepage
|
||||||
|
end
|
||||||
|
|
||||||
|
it "can send a new message to the bot" do
|
||||||
|
topic_page.visit_topic(pm)
|
||||||
|
topic_page.click_reply_button
|
||||||
|
expect(composer).to be_opened
|
||||||
|
|
||||||
|
composer.fill_in(with: "Hello bot replying to you")
|
||||||
|
composer.submit
|
||||||
|
expect(page).to have_content("Hello bot replying to you")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
29
spec/system/page_objects/components/ai_pm_homepage.rb
Normal file
29
spec/system/page_objects/components/ai_pm_homepage.rb
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module PageObjects
|
||||||
|
module Components
|
||||||
|
class AiPmHomepage < PageObjects::Components::Base
|
||||||
|
HOMEPAGE_WRAPPER_CLASS = ".ai-bot-conversations__content-wrapper"
|
||||||
|
|
||||||
|
def input
|
||||||
|
page.find("#ai-bot-conversations-input")
|
||||||
|
end
|
||||||
|
|
||||||
|
def submit
|
||||||
|
page.find(".ai-conversation-submit").click
|
||||||
|
end
|
||||||
|
|
||||||
|
def has_too_short_dialog?
|
||||||
|
page.find(".dialog-content", text: "Message must be longer than 10 characters")
|
||||||
|
end
|
||||||
|
|
||||||
|
def has_homepage?
|
||||||
|
page.has_css?(HOMEPAGE_WRAPPER_CLASS)
|
||||||
|
end
|
||||||
|
|
||||||
|
def has_no_homepage?
|
||||||
|
page.has_no_css?(HOMEPAGE_WRAPPER_CLASS)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -50,6 +50,8 @@ acceptance("AI Helper - Post Helper Menu", function (needs) {
|
|||||||
done: false,
|
done: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.get("/discourse-ai/ai-bot/conversations.json", () => {});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("displays streamed explanation", async function (assert) {
|
test("displays streamed explanation", async function (assert) {
|
||||||
|
@ -29,6 +29,8 @@ acceptance("Topic - Summary", function (needs) {
|
|||||||
done: false,
|
done: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.get("/discourse-ai/ai-bot/conversations.json", () => {});
|
||||||
});
|
});
|
||||||
|
|
||||||
needs.hooks.beforeEach(() => {
|
needs.hooks.beforeEach(() => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user