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

Commit 4854383

Browse files
committed
merge main in
2 parents 476ab50 + 7dc3c30 commit 4854383

File tree

13 files changed

+396
-265
lines changed

13 files changed

+396
-265
lines changed

.discourse-compatibility

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
< 3.5.0.beta3-dev: 09a68414804a1447f52e5d60691ba59742cda9ec
12
< 3.5.0.beta2-dev: de8624416a15b3d8e7ad350b083cc1420451ccec
23
< 3.5.0.beta1-dev: bdef136080074a993e7c4f5ca562edc31a8ba756
34
< 3.4.0.beta4-dev: a53719ab8eb071459f215227421b3ea4987e5f87

assets/javascripts/discourse/components/ai-persona-llm-selector.gjs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -148,21 +148,24 @@ export default class AiPersonaLlmSelector extends Component {
148148
this.setAllowLLMSelector();
149149

150150
if (this.hasLlmSelector) {
151-
let llm = this.keyValueStore.getItem(LLM_SELECTOR_KEY);
151+
let llmId = this.keyValueStore.getItem(LLM_SELECTOR_KEY);
152+
if (llmId) {
153+
llmId = parseInt(llmId, 10);
154+
}
152155

153156
const llmOption =
154-
this.llmOptions.find((innerLlmOption) => innerLlmOption.id === llm) ||
157+
this.llmOptions.find((innerLlmOption) => innerLlmOption.id === llmId) ||
155158
this.llmOptions[0];
156159

157160
if (llmOption) {
158-
llm = llmOption.id;
161+
llmId = llmOption.id;
159162
} else {
160-
llm = "";
163+
llmId = "";
161164
}
162165

163-
if (llm) {
166+
if (llmId) {
164167
next(() => {
165-
this.currentLlm = llm;
168+
this.currentLlm = llmId;
166169
});
167170
}
168171
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import Component from "@glimmer/component";
2+
import { isGPTBot } from "../../lib/ai-bot-helper";
3+
4+
export default class AiPersonaFlair extends Component {
5+
static shouldRender(args) {
6+
return isGPTBot(args.post.user);
7+
}
8+
9+
<template>
10+
<span class="persona-flair">
11+
{{@outletArgs.post.topic.ai_persona_name}}
12+
</span>
13+
</template>
14+
}

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,48 @@
11
import { ajax } from "discourse/lib/ajax";
22
import { popupAjaxError } from "discourse/lib/ajax-error";
3+
import { getOwnerWithFallback } from "discourse/lib/get-owner";
34
import Composer from "discourse/models/composer";
45
import { i18n } from "discourse-i18n";
56
import ShareFullTopicModal from "../components/modal/share-full-topic-modal";
67

78
const MAX_PERSONA_USER_ID = -1200;
89

10+
let enabledChatBotMap = null;
11+
12+
function ensureBotMap() {
13+
if (!enabledChatBotMap) {
14+
const currentUser = getOwnerWithFallback(this).lookup(
15+
"service:current-user"
16+
);
17+
enabledChatBotMap = {};
18+
currentUser.ai_enabled_chat_bots.forEach((bot) => {
19+
enabledChatBotMap[bot.id] = bot;
20+
});
21+
}
22+
}
23+
24+
export function isGPTBot(user) {
25+
if (!user) {
26+
return;
27+
}
28+
29+
ensureBotMap();
30+
return !!enabledChatBotMap[user.id];
31+
}
32+
33+
export function getBotType(user) {
34+
if (!user) {
35+
return;
36+
}
37+
38+
ensureBotMap();
39+
const bot = enabledChatBotMap[user.id];
40+
if (!bot) {
41+
return;
42+
}
43+
return bot.is_persona ? "persona" : "llm";
44+
}
45+
946
export function isPostFromAiBot(post, currentUser) {
1047
return (
1148
post.user_id <= MAX_PERSONA_USER_ID ||

assets/javascripts/initializers/ai-bot-replies.js

Lines changed: 37 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
11
import { hbs } from "ember-cli-htmlbars";
2+
import { withSilencedDeprecations } from "discourse/lib/deprecated";
23
import { withPluginApi } from "discourse/lib/plugin-api";
34
import { registerWidgetShim } from "discourse/widgets/render-glimmer";
45
import AiBotHeaderIcon from "../discourse/components/ai-bot-header-icon";
6+
import AiPersonaFlair from "../discourse/components/post/ai-persona-flair";
57
import AiCancelStreamingButton from "../discourse/components/post-menu/ai-cancel-streaming-button";
68
import AiDebugButton from "../discourse/components/post-menu/ai-debug-button";
79
import AiShareButton from "../discourse/components/post-menu/ai-share-button";
8-
import { showShareConversationModal } from "../discourse/lib/ai-bot-helper";
10+
import {
11+
getBotType,
12+
isGPTBot,
13+
showShareConversationModal,
14+
} from "../discourse/lib/ai-bot-helper";
915
import { streamPostText } from "../discourse/lib/ai-streamer/progress-handlers";
1016

11-
let enabledChatBotIds = [];
1217
let allowDebug = false;
1318

14-
function isGPTBot(user) {
15-
return user && enabledChatBotIds.includes(user.id);
16-
}
17-
1819
function attachHeaderIcon(api) {
1920
api.headerIcons.add("ai", AiBotHeaderIcon);
2021
}
@@ -53,28 +54,34 @@ function initializeAIBotReplies(api) {
5354
}
5455

5556
function initializePersonaDecorator(api) {
56-
let topicController = null;
57+
api.renderAfterWrapperOutlet("post-meta-data-poster-name", AiPersonaFlair);
58+
59+
withSilencedDeprecations("discourse.post-stream-widget-overrides", () =>
60+
initializeWidgetPersonaDecorator(api)
61+
);
62+
}
63+
64+
function initializeWidgetPersonaDecorator(api) {
5765
api.decorateWidget(`poster-name:after`, (dec) => {
58-
if (!isGPTBot(dec.attrs.user)) {
59-
return;
66+
const botType = getBotType(dec.attrs.user);
67+
// we have 2 ways of decorating
68+
// 1. if a bot is a LLM we decorate with persona name
69+
// 2. if bot is a persona we decorate with LLM name
70+
if (botType === "llm") {
71+
return dec.widget.attach("persona-flair", {
72+
personaName: dec.model?.topic?.ai_persona_name,
73+
});
74+
} else if (botType === "persona") {
75+
return dec.widget.attach("persona-flair", {
76+
personaName: dec.model?.llm_name,
77+
});
6078
}
61-
// this is hacky and will need to change
62-
// trouble is we need to get the model for the topic
63-
// and it is not available in the decorator
64-
// long term this will not be a problem once we remove widgets and
65-
// have a saner structure for our model
66-
topicController =
67-
topicController || api.container.lookup("controller:topic");
68-
69-
return dec.widget.attach("persona-flair", {
70-
topicController,
71-
});
7279
});
7380

7481
registerWidgetShim(
7582
"persona-flair",
7683
"span.persona-flair",
77-
hbs`{{@data.topicController.model.ai_persona_name}}`
84+
hbs`{{@data.personaName}}`
7885
);
7986
}
8087

@@ -159,16 +166,16 @@ export default {
159166
const user = container.lookup("service:current-user");
160167

161168
if (user?.ai_enabled_chat_bots) {
162-
enabledChatBotIds = user.ai_enabled_chat_bots.map((bot) => bot.id);
163169
allowDebug = user.can_debug_ai_bot_conversations;
164-
withPluginApi("1.6.0", attachHeaderIcon);
165-
withPluginApi("1.34.0", initializeAIBotReplies);
166-
withPluginApi("1.6.0", initializePersonaDecorator);
167-
withPluginApi("1.34.0", (api) => initializeDebugButton(api, container));
168-
withPluginApi("1.34.0", (api) => initializeShareButton(api, container));
169-
withPluginApi("1.22.0", (api) =>
170-
initializeShareTopicButton(api, container)
171-
);
170+
171+
withPluginApi((api) => {
172+
attachHeaderIcon(api);
173+
initializeAIBotReplies(api);
174+
initializePersonaDecorator(api);
175+
initializeDebugButton(api, container);
176+
initializeShareButton(api, container);
177+
initializeShareTopicButton(api, container);
178+
});
172179
}
173180
},
174181
};

assets/stylesheets/modules/ai-bot/common/bot-replies.scss

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,6 @@ article.streaming nav.post-controls .actions button.cancel-streaming {
7878
.topic-body .persona-flair {
7979
order: 2;
8080
font-size: var(--font-down-1);
81-
padding-top: 3px;
8281
}
8382

8483
details.ai-quote {

config/locales/server.en.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ en:
113113
ai_discord_search_mode: "Select the search mode to use for Discord search"
114114
ai_discord_search_persona: "The persona to use for Discord search."
115115
ai_discord_allowed_guilds: "Discord guilds (servers) where the bot is allowed to search"
116+
ai_enable_experimental_bot_ux: "Enable experimental bot UI that allows for a more dedicated experience"
116117

117118
reviewables:
118119
reasons:

lib/ai_bot/entry_point.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ module DiscourseAi
44
module AiBot
55
USER_AGENT = "Discourse AI Bot 1.0 (https://www.discourse.org)"
66
TOPIC_AI_BOT_PM_FIELD = "is_ai_bot_pm"
7+
POST_AI_LLM_NAME_FIELD = "ai_llm_name"
78

89
class EntryPoint
910
Bot = Struct.new(:id, :name, :llm)
@@ -65,6 +66,10 @@ def self.ai_share_error(topic, guardian)
6566
end
6667

6768
def inject_into(plugin)
69+
# Long term we need a better API here
70+
# we only want to load this custom field for bots
71+
TopicView.default_post_custom_fields << POST_AI_LLM_NAME_FIELD
72+
6873
plugin.register_topic_custom_field_type(TOPIC_AI_BOT_PM_FIELD, :string)
6974

7075
plugin.on(:topic_created) do |topic|
@@ -139,6 +144,14 @@ def inject_into(plugin)
139144
end,
140145
) { true }
141146

147+
plugin.add_to_serializer(
148+
:post,
149+
:llm_name,
150+
include_condition: -> do
151+
object.topic.private_message? && object.custom_fields[POST_AI_LLM_NAME_FIELD]
152+
end,
153+
) { object.custom_fields[POST_AI_LLM_NAME_FIELD] }
154+
142155
plugin.add_to_serializer(
143156
:current_user,
144157
:ai_enabled_personas,

lib/ai_bot/playground.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,9 @@ def reply_to(
458458
skip_jobs: true,
459459
post_type: post_type,
460460
skip_guardian: true,
461+
custom_fields: {
462+
DiscourseAi::AiBot::POST_AI_LLM_NAME_FIELD => bot.llm.llm_model.name,
463+
},
461464
)
462465

463466
publish_update(reply_post, { raw: reply_post.cooked })

lib/inference/open_ai_image_generator.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ module ::DiscourseAi
44
module Inference
55
class OpenAiImageGenerator
66
TIMEOUT = 60
7+
MAX_IMAGE_SIZE = 20_971_520 # 20MB (technically 25 is supported by API)
78

89
def self.create_uploads!(
910
prompts,

0 commit comments

Comments
 (0)