Skip to content

Commit 731fb4e

Browse files
markvanlanjanzenisaac
authored andcommitted
FEATURE: Bot Conversation Homepage
1 parent 0c8718e commit 731fb4e

File tree

21 files changed

+1274
-16
lines changed

21 files changed

+1274
-16
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# frozen_string_literal: true
2+
3+
module DiscourseAi
4+
module AiBot
5+
class ConversationsController < ::ApplicationController
6+
requires_plugin ::DiscourseAi::PLUGIN_NAME
7+
requires_login
8+
9+
def index
10+
page = params[:page].to_i
11+
per_page = params[:per_page]&.to_i || 40
12+
13+
bot_user_ids = EntryPoint.all_bot_ids
14+
base_query =
15+
Topic
16+
.private_messages_for_user(current_user)
17+
.joins(:topic_users)
18+
.where(topic_users: { user_id: bot_user_ids })
19+
.distinct
20+
total = base_query.count
21+
pms = base_query.order(last_posted_at: :desc).offset(page * per_page).limit(per_page)
22+
23+
render json: {
24+
conversations: serialize_data(pms, BasicTopicSerializer),
25+
meta: {
26+
total: total,
27+
page: page,
28+
per_page: per_page,
29+
more: total > (page + 1) * per_page,
30+
},
31+
}
32+
end
33+
end
34+
end
35+
end

assets/javascripts/discourse/components/ai-bot-header-icon.gjs

+4
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export default class AiBotHeaderIcon extends Component {
99
@service currentUser;
1010
@service siteSettings;
1111
@service composer;
12+
@service router;
1213

1314
get bots() {
1415
const availableBots = this.currentUser.ai_enabled_chat_bots
@@ -24,6 +25,9 @@ export default class AiBotHeaderIcon extends Component {
2425

2526
@action
2627
compose() {
28+
if (this.siteSettings.ai_enable_experimental_bot_ux) {
29+
return this.router.transitionTo("discourse-ai-bot-conversations");
30+
}
2731
composeAiBotMessage(this.bots[0], this.composer);
2832
}
2933

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import Component from "@glimmer/component";
2+
import { service } from "@ember/service";
3+
import DButton from "discourse/components/d-button";
4+
import { AI_CONVERSATIONS_PANEL } from "../services/ai-conversations-sidebar-manager";
5+
6+
export default class AiBotSidebarNewConversation extends Component {
7+
@service router;
8+
@service sidebarState;
9+
10+
get shouldRender() {
11+
return (
12+
this.router.currentRouteName !== "discourse-ai-bot-conversations" &&
13+
this.sidebarState.isCurrentPanel(AI_CONVERSATIONS_PANEL)
14+
);
15+
}
16+
17+
<template>
18+
{{#if this.shouldRender}}
19+
<DButton
20+
@route="/discourse-ai/ai-bot/conversations"
21+
@label="discourse_ai.ai_bot.conversations.new"
22+
@icon="plus"
23+
class="ai-new-question-button btn-default"
24+
/>
25+
{{/if}}
26+
</template>
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import Component from "@glimmer/component";
2+
import { service } from "@ember/service";
3+
import bodyClass from "discourse/helpers/body-class";
4+
5+
export default class AiBotConversation extends Component {
6+
@service siteSettings;
7+
8+
get show() {
9+
return (
10+
this.siteSettings.ai_enable_experimental_bot_ux &&
11+
this.args.outletArgs.model?.pm_with_non_human_user
12+
);
13+
}
14+
15+
<template>
16+
{{#if this.show}}
17+
{{bodyClass "discourse-ai-bot-conversations-page"}}
18+
{{/if}}
19+
</template>
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import Controller from "@ember/controller";
2+
import { action } from "@ember/object";
3+
import { service } from "@ember/service";
4+
5+
export default class DiscourseAiBotConversations extends Controller {
6+
@service aiBotConversationsHiddenSubmit;
7+
8+
textarea = null;
9+
10+
@action
11+
updateInputValue(event) {
12+
this._autoExpandTextarea();
13+
this.aiBotConversationsHiddenSubmit.inputValue = event.target.value;
14+
}
15+
16+
@action
17+
handleKeyDown(event) {
18+
if (event.key === "Enter" && !event.shiftKey) {
19+
this.aiBotConversationsHiddenSubmit.submitToBot();
20+
}
21+
}
22+
23+
@action
24+
setTextArea(element) {
25+
this.textarea = element;
26+
}
27+
28+
_autoExpandTextarea() {
29+
this.textarea.style.height = "auto";
30+
this.textarea.style.height = this.textarea.scrollHeight + "px";
31+
}
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export default function () {
2+
this.route("discourse-ai-bot-conversations", {
3+
path: "/discourse-ai/ai-bot/conversations",
4+
});
5+
}

assets/javascripts/discourse/lib/ai-bot-helper.js

+29-16
Original file line numberDiff line numberDiff line change
@@ -23,24 +23,37 @@ export function showShareConversationModal(modal, topicId) {
2323
.catch(popupAjaxError);
2424
}
2525

26-
export function composeAiBotMessage(targetBot, composer) {
26+
export async function composeAiBotMessage(
27+
targetBot,
28+
composer,
29+
options = {
30+
skipFocus: false,
31+
topicBody: "",
32+
}
33+
) {
2734
const currentUser = composer.currentUser;
2835
const draftKey = "new_private_message_ai_" + new Date().getTime();
2936

30-
let botUsername = currentUser.ai_enabled_chat_bots.find(
31-
(bot) => bot.model_name === targetBot
32-
).username;
37+
const botUsername = !targetBot
38+
? currentUser.ai_enabled_chat_bots[0].username
39+
: currentUser.ai_enabled_chat_bots.find(
40+
(bot) => bot.model_name === targetBot
41+
)?.username;
3342

34-
composer.focusComposer({
35-
fallbackToNewTopic: true,
36-
openOpts: {
37-
action: Composer.PRIVATE_MESSAGE,
38-
recipients: botUsername,
39-
topicTitle: i18n("discourse_ai.ai_bot.default_pm_prefix"),
40-
archetypeId: "private_message",
41-
draftKey,
42-
hasGroups: false,
43-
warningsDisabled: true,
44-
},
45-
});
43+
const data = {
44+
action: Composer.PRIVATE_MESSAGE,
45+
recipients: botUsername,
46+
topicTitle: i18n("discourse_ai.ai_bot.default_pm_prefix"),
47+
archetypeId: "private_message",
48+
draftKey,
49+
hasGroups: false,
50+
warningsDisabled: true,
51+
};
52+
53+
if (options.skipFocus) {
54+
data.topicBody = options.topicBody;
55+
await composer.open(data);
56+
} else {
57+
composer.focusComposer({ fallbackToNewTopic: true, openOpts: data });
58+
}
4659
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import DiscourseRoute from "discourse/routes/discourse";
2+
3+
export default class DiscourseAiBotConversationsRoute extends DiscourseRoute {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { action } from "@ember/object";
2+
import { next } from "@ember/runloop";
3+
import Service, { service } from "@ember/service";
4+
import { i18n } from "discourse-i18n";
5+
import { composeAiBotMessage } from "../lib/ai-bot-helper";
6+
7+
export default class AiBotConversationsHiddenSubmit extends Service {
8+
@service composer;
9+
@service aiConversationsSidebarManager;
10+
@service dialog;
11+
12+
inputValue = "";
13+
14+
@action
15+
focusInput() {
16+
this.composer.destroyDraft();
17+
this.composer.close();
18+
next(() => {
19+
document.getElementById("custom-homepage-input").focus();
20+
});
21+
}
22+
23+
@action
24+
async submitToBot() {
25+
this.composer.destroyDraft();
26+
this.composer.close();
27+
28+
if (this.inputValue.length < 10) {
29+
return this.dialog.alert({
30+
message: i18n(
31+
"discourse_ai.ai_bot.conversations.min_input_length_message"
32+
),
33+
didConfirm: () => this.focusInput(),
34+
didCancel: () => this.focusInput(),
35+
});
36+
}
37+
38+
// we are intentionally passing null as the targetBot to allow for the
39+
// function to select the first available bot. This will be refactored in the
40+
// future to allow for selecting a specific bot.
41+
await composeAiBotMessage(null, this.composer, {
42+
skipFocus: true,
43+
topicBody: this.inputValue,
44+
});
45+
46+
try {
47+
await this.composer.save();
48+
this.aiConversationsSidebarManager.newTopicForceSidebar = true;
49+
if (this.inputValue.length > 10) {
50+
// prevents submitting same message again when returning home
51+
// but avoids deleting too-short message on submit
52+
this.inputValue = "";
53+
}
54+
} catch (error) {
55+
// eslint-disable-next-line no-console
56+
console.error("Failed to submit message:", error);
57+
}
58+
}
59+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { tracked } from "@glimmer/tracking";
2+
import { alias } from "@ember/object/computed";
3+
import Service, { service } from "@ember/service";
4+
import { MAIN_PANEL } from "discourse/lib/sidebar/panels";
5+
6+
export const AI_CONVERSATIONS_PANEL = "ai-conversations";
7+
8+
export default class AiConversationsSidebarManager extends Service {
9+
@service sidebarState;
10+
11+
@tracked newTopicForceSidebar = false;
12+
@alias("sidebarState.isForcingSidebar") isForcingSidebar;
13+
14+
forceCustomSidebar() {
15+
// Set the panel to your custom panel
16+
this.sidebarState.setPanel(AI_CONVERSATIONS_PANEL);
17+
18+
// Use separated mode to ensure independence from hamburger menu
19+
this.sidebarState.setSeparatedMode();
20+
21+
// Hide panel switching buttons to keep UI clean
22+
this.sidebarState.hideSwitchPanelButtons();
23+
24+
this.isForcingSidebar = true;
25+
document.body.classList.add("has-ai-conversations-sidebar");
26+
return true;
27+
}
28+
29+
stopForcingCustomSidebar() {
30+
// This method is called when leaving your route
31+
// Only restore main panel if we previously forced ours
32+
document.body.classList.remove("has-ai-conversations-sidebar");
33+
if (this.isForcingSidebar) {
34+
this.sidebarState.setPanel(MAIN_PANEL); // Return to main sidebar panel
35+
this.isForcingSidebar = false;
36+
}
37+
}
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{{body-class "discourse-ai-bot-conversations-page"}}
2+
3+
<div class="custom-homepage__content-wrapper">
4+
<h1>{{i18n "discourse_ai.ai_bot.conversations.header"}}</h1>
5+
<div class="custom-homepage__input-wrapper">
6+
<textarea
7+
{{didInsert this.setTextArea}}
8+
{{on "input" this.updateInputValue}}
9+
{{on "keydown" this.handleKeyDown}}
10+
id="custom-homepage-input"
11+
placeholder={{i18n "discourse_ai.ai_bot.conversations.placeholder"}}
12+
minlength="10"
13+
rows="1"
14+
/>
15+
<DButton
16+
@action={{this.aiBotConversationsHiddenSubmit.submitToBot}}
17+
@icon="paper-plane"
18+
@title="discourse_ai.ai_bot.conversations.header"
19+
class="ai-bot-button btn-primary ai-conversation-submit"
20+
/>
21+
</div>
22+
<p class="ai-disclaimer">
23+
{{i18n "discourse_ai.ai_bot.conversations.disclaimer"}}
24+
</p>
25+
</div>

0 commit comments

Comments
 (0)