Skip to content
This repository was archived by the owner on Jul 22, 2025. It is now read-only.

Commit 05f997e

Browse files
markvanlanjanzenisaac
authored andcommitted
Mega WIP version
1 parent 38b4925 commit 05f997e

File tree

19 files changed

+1105
-0
lines changed

19 files changed

+1105
-0
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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+
# Step 1: Retrieve all AI bot user IDs
11+
bot_user_ids = EntryPoint.all_bot_ids
12+
13+
# Step 2: Query for PM topics including current_user and any bot ID
14+
pms =
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+
21+
# Step 3: Serialize (empty array if no results)
22+
serialized_pms = serialize_data(pms, BasicTopicSerializer)
23+
24+
render json: serialized_pms, status: 200
25+
end
26+
end
27+
end
28+
end

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

Lines changed: 4 additions & 0 deletions
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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import Component from "@glimmer/component";
2+
import { service } from "@ember/service";
3+
import DButton from "discourse/components/d-button";
4+
5+
export default class AiBotSidebarNewConversation extends Component {
6+
@service router;
7+
8+
get show() {
9+
// don't show the new question button on the conversations home page
10+
return this.router.currentRouteName !== "discourse-ai-bot-conversations";
11+
}
12+
13+
<template>
14+
{{#if this.show}}
15+
<DButton
16+
@route="/discourse-ai/ai-bot/conversations"
17+
@label="discourse_ai.ai_bot.conversations.new"
18+
@icon="plus"
19+
class="ai-new-question-button btn-default"
20+
/>
21+
{{/if}}
22+
</template>
23+
}
Lines changed: 20 additions & 0 deletions
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+
}
Lines changed: 32 additions & 0 deletions
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+
}
Lines changed: 5 additions & 0 deletions
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+
}
Lines changed: 3 additions & 0 deletions
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 {}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { action } from "@ember/object";
2+
import { next } from "@ember/runloop";
3+
import Service, { service } from "@ember/service";
4+
import Composer from "discourse/models/composer";
5+
import { i18n } from "discourse-i18n";
6+
7+
export default class AiBotConversationsHiddenSubmit extends Service {
8+
@service composer;
9+
@service dialog;
10+
11+
inputValue = "";
12+
13+
@action
14+
focusInput() {
15+
this.composer.destroyDraft();
16+
this.composer.close();
17+
next(() => {
18+
document.getElementById("custom-homepage-input").focus();
19+
});
20+
}
21+
22+
@action
23+
async submitToBot() {
24+
this.composer.destroyDraft();
25+
this.composer.close();
26+
27+
if (this.inputValue.length < 10) {
28+
return this.dialog.alert({
29+
message: i18n(
30+
"discourse_ai.ai_bot.conversations.min_input_length_message"
31+
),
32+
didConfirm: () => this.focusInput(),
33+
didCancel: () => this.focusInput(),
34+
});
35+
}
36+
37+
// borrowed from ai-bot-helper.js
38+
const draftKey = "new_private_message_ai_" + new Date().getTime();
39+
40+
const personaWithUsername = this.currentUser.ai_enabled_personas.find(
41+
(persona) => persona.username
42+
);
43+
// this is a total hack, the composer is hidden on the homepage with CSS
44+
await this.composer.open({
45+
action: Composer.PRIVATE_MESSAGE,
46+
draftKey,
47+
recipients: personaWithUsername.username,
48+
topicTitle: i18n("discourse_ai.ai_bot.default_pm_prefix"),
49+
topicBody: this.inputValue,
50+
archetypeId: "private_message",
51+
disableDrafts: true,
52+
});
53+
54+
try {
55+
await this.composer.save();
56+
if (this.inputValue.length > 10) {
57+
// prevents submitting same message again when returning home
58+
// but avoids deleting too-short message on submit
59+
this.inputValue = "";
60+
}
61+
} catch (error) {
62+
// eslint-disable-next-line no-console
63+
console.error("Failed to submit message:", error);
64+
}
65+
}
66+
}
Lines changed: 25 additions & 0 deletions
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>
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { tracked } from "@glimmer/tracking";
2+
import { ajax } from "discourse/lib/ajax";
3+
import { withPluginApi } from "discourse/lib/plugin-api";
4+
import { i18n } from "discourse-i18n";
5+
import AiBotSidebarNewConversation from "../discourse/components/ai-bot-sidebar-new-conversation";
6+
7+
export default {
8+
name: "custom-sidebar-bot-messages",
9+
initialize() {
10+
withPluginApi("1.37.1", (api) => {
11+
const currentUser = api.container.lookup("service:current-user");
12+
const appEvents = api.container.lookup("service:app-events");
13+
const messageBus = api.container.lookup("service:message-bus");
14+
15+
if (!currentUser) {
16+
return;
17+
}
18+
19+
// TODO: Replace
20+
const recentConversations = 10;
21+
22+
api.renderInOutlet("after-sidebar-sections", AiBotSidebarNewConversation);
23+
24+
api.addSidebarSection(
25+
(BaseCustomSidebarSection, BaseCustomSidebarSectionLink) => {
26+
return class extends BaseCustomSidebarSection {
27+
@tracked links = [];
28+
@tracked topics = [];
29+
isFetching = false;
30+
totalTopicsCount = 0;
31+
32+
constructor() {
33+
super(...arguments);
34+
this.fetchMessages();
35+
36+
appEvents.on("topic:created", (topic) => {
37+
// when asking a new question
38+
this.addNewMessage(topic);
39+
this.watchForTitleUpdate(topic);
40+
});
41+
}
42+
43+
fetchMessages() {
44+
if (this.isFetching) {
45+
return;
46+
}
47+
48+
this.isFetching = true;
49+
50+
ajax("/discourse-ai/ai-bot/conversations.json")
51+
.then((data) => {
52+
this.topics = data.conversations.slice(
53+
0,
54+
recentConversations
55+
);
56+
this.isFetching = false;
57+
this.buildSidebarLinks();
58+
})
59+
.catch(() => (this.isFetching = false));
60+
}
61+
62+
addNewMessage(newTopic) {
63+
// the pm endpoint isn't fast enough include the newly created topic
64+
// so this adds the new topic to the existing list
65+
const builtTopic =
66+
new (class extends BaseCustomSidebarSectionLink {
67+
name = newTopic.title;
68+
route = "topic.fromParamsNear";
69+
models = [newTopic.topic_slug, newTopic.topic_id, 0];
70+
title = newTopic.title;
71+
text = newTopic.title;
72+
prefixType = "icon";
73+
prefixValue = "robot";
74+
})();
75+
76+
this.links = [builtTopic, ...this.links];
77+
}
78+
79+
buildSidebarLinks() {
80+
this.links = this.topics.map((topic) => {
81+
return new (class extends BaseCustomSidebarSectionLink {
82+
name = topic.title;
83+
route = "topic.fromParamsNear";
84+
models = [
85+
topic.slug,
86+
topic.id,
87+
topic.last_read_post_number || 0,
88+
];
89+
title = topic.title;
90+
text = topic.title;
91+
prefixType = "icon";
92+
prefixValue = "robot";
93+
})();
94+
});
95+
96+
if (this.totalTopicsCount > recentConversations) {
97+
this.links.push(
98+
new (class extends BaseCustomSidebarSectionLink {
99+
name = "View All";
100+
route = "userPrivateMessages.user.index";
101+
models = [currentUser.username];
102+
title = "View all...";
103+
text = "View all...";
104+
prefixType = "icon";
105+
prefixValue = "list";
106+
})()
107+
);
108+
}
109+
}
110+
111+
watchForTitleUpdate(topic) {
112+
const channel = `/discourse-ai/ai-bot/topic/${topic.topic_id}`;
113+
messageBus.subscribe(channel, () => {
114+
this.fetchMessages();
115+
messageBus.unsubscribe(channel);
116+
});
117+
}
118+
119+
get name() {
120+
return "custom-messages";
121+
}
122+
123+
get text() {
124+
// TODO: FIX
125+
//return i18n(themePrefix("messages_sidebar.title"));
126+
return "Conversations";
127+
}
128+
129+
get displaySection() {
130+
return this.links?.length > 0;
131+
}
132+
};
133+
}
134+
);
135+
});
136+
},
137+
};

0 commit comments

Comments
 (0)