From ede65c971ff593c9c3c19562e4f1c2b453c3d146 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Saquetim?= <1108771+megothss@users.noreply.github.com> Date: Tue, 29 Apr 2025 23:55:54 -0300 Subject: [PATCH] DEV: Add compatibility with the Glimmer Post Stream (#1230) --- .discourse-compatibility | 1 + .../components/post/ai-persona-flair.gjs | 14 + .../discourse/lib/ai-bot-helper.js | 18 + .../initializers/ai-bot-replies.js | 50 +-- spec/system/ai_bot/homepage_spec.rb | 378 +++++++++--------- 5 files changed, 250 insertions(+), 211 deletions(-) create mode 100644 assets/javascripts/discourse/components/post/ai-persona-flair.gjs diff --git a/.discourse-compatibility b/.discourse-compatibility index f1303357..73df02b1 100644 --- a/.discourse-compatibility +++ b/.discourse-compatibility @@ -1,3 +1,4 @@ +< 3.5.0.beta3-dev: 09a68414804a1447f52e5d60691ba59742cda9ec < 3.5.0.beta2-dev: de8624416a15b3d8e7ad350b083cc1420451ccec < 3.5.0.beta1-dev: bdef136080074a993e7c4f5ca562edc31a8ba756 < 3.4.0.beta4-dev: a53719ab8eb071459f215227421b3ea4987e5f87 diff --git a/assets/javascripts/discourse/components/post/ai-persona-flair.gjs b/assets/javascripts/discourse/components/post/ai-persona-flair.gjs new file mode 100644 index 00000000..af637159 --- /dev/null +++ b/assets/javascripts/discourse/components/post/ai-persona-flair.gjs @@ -0,0 +1,14 @@ +import Component from "@glimmer/component"; +import { isGPTBot } from "../../lib/ai-bot-helper"; + +export default class AiPersonaFlair extends Component { + static shouldRender(args) { + return isGPTBot(args.post.user); + } + + +} diff --git a/assets/javascripts/discourse/lib/ai-bot-helper.js b/assets/javascripts/discourse/lib/ai-bot-helper.js index d7fb0132..9ab3022f 100644 --- a/assets/javascripts/discourse/lib/ai-bot-helper.js +++ b/assets/javascripts/discourse/lib/ai-bot-helper.js @@ -1,11 +1,29 @@ import { ajax } from "discourse/lib/ajax"; import { popupAjaxError } from "discourse/lib/ajax-error"; +import { getOwnerWithFallback } from "discourse/lib/get-owner"; import Composer from "discourse/models/composer"; import { i18n } from "discourse-i18n"; import ShareFullTopicModal from "../components/modal/share-full-topic-modal"; const MAX_PERSONA_USER_ID = -1200; +let enabledChatBotIds; + +export function isGPTBot(user) { + if (!user) { + return; + } + + if (!enabledChatBotIds) { + const currentUser = getOwnerWithFallback(this).lookup( + "service:current-user" + ); + enabledChatBotIds = currentUser.ai_enabled_chat_bots.map((bot) => bot.id); + } + + return enabledChatBotIds.includes(user.id); +} + export function isPostFromAiBot(post, currentUser) { return ( post.user_id <= MAX_PERSONA_USER_ID || diff --git a/assets/javascripts/initializers/ai-bot-replies.js b/assets/javascripts/initializers/ai-bot-replies.js index e2187845..e4f7bf1c 100644 --- a/assets/javascripts/initializers/ai-bot-replies.js +++ b/assets/javascripts/initializers/ai-bot-replies.js @@ -1,20 +1,20 @@ import { hbs } from "ember-cli-htmlbars"; +import { withSilencedDeprecations } from "discourse/lib/deprecated"; import { withPluginApi } from "discourse/lib/plugin-api"; import { registerWidgetShim } from "discourse/widgets/render-glimmer"; import AiBotHeaderIcon from "../discourse/components/ai-bot-header-icon"; +import AiPersonaFlair from "../discourse/components/post/ai-persona-flair"; import AiCancelStreamingButton from "../discourse/components/post-menu/ai-cancel-streaming-button"; import AiDebugButton from "../discourse/components/post-menu/ai-debug-button"; import AiShareButton from "../discourse/components/post-menu/ai-share-button"; -import { showShareConversationModal } from "../discourse/lib/ai-bot-helper"; +import { + isGPTBot, + showShareConversationModal, +} from "../discourse/lib/ai-bot-helper"; import { streamPostText } from "../discourse/lib/ai-streamer/progress-handlers"; -let enabledChatBotIds = []; let allowDebug = false; -function isGPTBot(user) { - return user && enabledChatBotIds.includes(user.id); -} - function attachHeaderIcon(api) { api.headerIcons.add("ai", AiBotHeaderIcon); } @@ -53,28 +53,28 @@ function initializeAIBotReplies(api) { } function initializePersonaDecorator(api) { - let topicController = null; + api.renderAfterWrapperOutlet("post-meta-data-poster-name", AiPersonaFlair); + + withSilencedDeprecations("discourse.post-stream-widget-overrides", () => + initializeWidgetPersonaDecorator(api) + ); +} + +function initializeWidgetPersonaDecorator(api) { api.decorateWidget(`poster-name:after`, (dec) => { if (!isGPTBot(dec.attrs.user)) { return; } - // this is hacky and will need to change - // trouble is we need to get the model for the topic - // and it is not available in the decorator - // long term this will not be a problem once we remove widgets and - // have a saner structure for our model - topicController = - topicController || api.container.lookup("controller:topic"); return dec.widget.attach("persona-flair", { - topicController, + personaName: dec.model?.topic?.ai_persona_name, }); }); registerWidgetShim( "persona-flair", "span.persona-flair", - hbs`{{@data.topicController.model.ai_persona_name}}` + hbs`{{@data.personaName}}` ); } @@ -159,16 +159,16 @@ export default { const user = container.lookup("service:current-user"); if (user?.ai_enabled_chat_bots) { - enabledChatBotIds = user.ai_enabled_chat_bots.map((bot) => bot.id); allowDebug = user.can_debug_ai_bot_conversations; - withPluginApi("1.6.0", attachHeaderIcon); - withPluginApi("1.34.0", initializeAIBotReplies); - withPluginApi("1.6.0", initializePersonaDecorator); - withPluginApi("1.34.0", (api) => initializeDebugButton(api, container)); - withPluginApi("1.34.0", (api) => initializeShareButton(api, container)); - withPluginApi("1.22.0", (api) => - initializeShareTopicButton(api, container) - ); + + withPluginApi((api) => { + attachHeaderIcon(api); + initializeAIBotReplies(api); + initializePersonaDecorator(api); + initializeDebugButton(api, container); + initializeShareButton(api, container); + initializeShareTopicButton(api, container); + }); } }, }; diff --git a/spec/system/ai_bot/homepage_spec.rb b/spec/system/ai_bot/homepage_spec.rb index 009f34e7..635ed24d 100644 --- a/spec/system/ai_bot/homepage_spec.rb +++ b/spec/system/ai_bot/homepage_spec.rb @@ -105,230 +105,236 @@ RSpec.describe "AI Bot - Homepage", type: :system do sign_in(user) end - context "when `ai_enable_experimental_bot_ux` is enabled" do - it "renders landing page on bot click" do - visit "/" - header.click_bot_button - expect(ai_pm_homepage).to have_homepage - expect(sidebar).to be_visible - end + %w[enabled disabled].each do |value| + before { SiteSetting.glimmer_post_stream_mode = value } - it "displays error when message is too short" do - visit "/" - header.click_bot_button + context "when glimmer_post_stream_mode=#{value}" do + context "when `ai_enable_experimental_bot_ux` is enabled" do + it "renders landing page on bot click" do + visit "/" + header.click_bot_button + expect(ai_pm_homepage).to have_homepage + expect(sidebar).to be_visible + end - 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 "displays error when message is too short" do + visit "/" + header.click_bot_button - it "hides default content in the sidebar" do - visit "/" - header.click_bot_button + 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 - 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 "hides default content in the sidebar" do + visit "/" + header.click_bot_button - it "shows the bot conversation in the sidebar" do - visit "/" - header.click_bot_button + 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 - expect(ai_pm_homepage).to have_homepage - expect(sidebar).to have_section("ai-conversations-history") - expect(sidebar).to have_section_link("Today") - expect(sidebar).to have_section_link(pm.title) - expect(sidebar).to have_no_css("button.ai-new-question-button") - end + it "shows the bot conversation in the sidebar" do + visit "/" + header.click_bot_button - it "displays last_7_days label in the sidebar" do - pm.update!(last_posted_at: Time.zone.now - 5.days) - visit "/" - header.click_bot_button + expect(ai_pm_homepage).to have_homepage + expect(sidebar).to have_section("ai-conversations-history") + expect(sidebar).to have_section_link("Today") + expect(sidebar).to have_section_link(pm.title) + expect(sidebar).to have_no_css("button.ai-new-question-button") + end - expect(ai_pm_homepage).to have_homepage - expect(sidebar).to have_section_link("Last 7 days") - end + it "displays last_7_days label in the sidebar" do + pm.update!(last_posted_at: Time.zone.now - 5.days) + visit "/" + header.click_bot_button - it "displays last_30_days label in the sidebar" do - pm.update!(last_posted_at: Time.zone.now - 28.days) - visit "/" - header.click_bot_button + expect(ai_pm_homepage).to have_homepage + expect(sidebar).to have_section_link("Last 7 days") + end - expect(ai_pm_homepage).to have_homepage - expect(sidebar).to have_section_link("Last 30 days") - end + it "displays last_30_days label in the sidebar" do + pm.update!(last_posted_at: Time.zone.now - 28.days) + visit "/" + header.click_bot_button - it "displays month and year label in the sidebar for older conversations" do - pm.update!(last_posted_at: "2024-04-10 15:39:11.406192000 +00:00") - visit "/" - header.click_bot_button + expect(ai_pm_homepage).to have_homepage + expect(sidebar).to have_section_link("Last 30 days") + end - expect(ai_pm_homepage).to have_homepage - expect(sidebar).to have_section_link("Apr 2024") - end + it "displays month and year label in the sidebar for older conversations" do + pm.update!(last_posted_at: "2024-04-10 15:39:11.406192000 +00:00") + visit "/" + header.click_bot_button - it "navigates to the bot conversation when clicked" do - visit "/" - header.click_bot_button + expect(ai_pm_homepage).to have_homepage + expect(sidebar).to have_section_link("Apr 2024") + end - expect(ai_pm_homepage).to have_homepage - ai_pm_homepage.click_fist_sidebar_conversation - expect(topic_page).to have_topic_title(pm.title) - end + it "navigates to the bot conversation when clicked" do + visit "/" + header.click_bot_button - it "displays the shuffle icon when on homepage or bot PM" do - visit "/" - expect(header).to have_icon_in_bot_button(icon: "robot") - header.click_bot_button + expect(ai_pm_homepage).to have_homepage + ai_pm_homepage.click_fist_sidebar_conversation + expect(topic_page).to have_topic_title(pm.title) + end - expect(header).to have_icon_in_bot_button(icon: "shuffle") + it "displays the shuffle icon when on homepage or bot PM" do + visit "/" + expect(header).to have_icon_in_bot_button(icon: "robot") + header.click_bot_button - # Go to a PM and assert that the icon is still shuffle - ai_pm_homepage.click_fist_sidebar_conversation - expect(header).to have_icon_in_bot_button(icon: "shuffle") + expect(header).to have_icon_in_bot_button(icon: "shuffle") - # Go back home and assert that the icon is now robot again - header.click_bot_button - expect(header).to have_icon_in_bot_button(icon: "robot") - end + # Go to a PM and assert that the icon is still shuffle + ai_pm_homepage.click_fist_sidebar_conversation + expect(header).to have_icon_in_bot_button(icon: "shuffle") - 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 + # Go back home and assert that the icon is now robot again + header.click_bot_button + expect(header).to have_icon_in_bot_button(icon: "robot") + 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 "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 "can send a new message to the bot" do - topic_page.visit_topic(pm) - topic_page.click_reply_button - expect(composer).to be_opened + 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 - composer.fill_in(with: "Hello bot replying to you") - composer.submit - expect(page).to have_content("Hello bot replying to you") - 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 - it "does not render custom sidebar on non-authored bot pms" do - # Include user_2 in the PM by creating a new post and topic_allowed_user association - Fabricate(:post, topic: pm, user: user_2, post_number: 4) - Fabricate(:topic_allowed_user, topic: pm, user: user_2) - sign_in(user_2) - topic_page.visit_topic(pm) + composer.fill_in(with: "Hello bot replying to you") + composer.submit + expect(page).to have_content("Hello bot replying to you") + end - expect(sidebar).to be_visible - expect(sidebar).to have_no_section("ai-conversations-history") - expect(sidebar).to have_no_css("button.ai-new-question-button") - end + it "does not render custom sidebar on non-authored bot pms" do + # Include user_2 in the PM by creating a new post and topic_allowed_user association + Fabricate(:post, topic: pm, user: user_2, post_number: 4) + Fabricate(:topic_allowed_user, topic: pm, user: user_2) + sign_in(user_2) + topic_page.visit_topic(pm) - it "does not include non-authored bot pms in sidebar" do - # Include user_2 in the PM by creating a new post and topic_allowed_user association - Fabricate(:post, topic: pm, user: user_2, post_number: 4) - Fabricate(:topic_allowed_user, topic: pm, user: user_2) - sign_in(user_2) + expect(sidebar).to be_visible + expect(sidebar).to have_no_section("ai-conversations-history") + expect(sidebar).to have_no_css("button.ai-new-question-button") + end - visit "/" - header.click_bot_button - expect(ai_pm_homepage).to have_homepage - expect(sidebar).to have_no_section_link(pm.title) - end + it "does not include non-authored bot pms in sidebar" do + # Include user_2 in the PM by creating a new post and topic_allowed_user association + Fabricate(:post, topic: pm, user: user_2, post_number: 4) + Fabricate(:topic_allowed_user, topic: pm, user: user_2) + sign_in(user_2) - it "Allows choosing persona and LLM" do - ai_pm_homepage.visit + visit "/" + header.click_bot_button + expect(ai_pm_homepage).to have_homepage + expect(sidebar).to have_no_section_link(pm.title) + end - ai_pm_homepage.persona_selector.expand - ai_pm_homepage.persona_selector.select_row_by_name(persona.name) - ai_pm_homepage.persona_selector.collapse + it "Allows choosing persona and LLM" do + ai_pm_homepage.visit - ai_pm_homepage.llm_selector.expand - ai_pm_homepage.llm_selector.select_row_by_name(claude_2_dup.display_name) - ai_pm_homepage.llm_selector.collapse - end + ai_pm_homepage.persona_selector.expand + ai_pm_homepage.persona_selector.select_row_by_name(persona.name) + ai_pm_homepage.persona_selector.collapse - it "renders back to forum link" do - ai_pm_homepage.visit - expect(ai_pm_homepage).to have_sidebar_back_link - end + ai_pm_homepage.llm_selector.expand + ai_pm_homepage.llm_selector.select_row_by_name(claude_2_dup.display_name) + ai_pm_homepage.llm_selector.collapse + end - context "with hamburger menu" do - before { SiteSetting.navigation_menu = "header dropdown" } - it "keeps robot icon in the header and doesn't display sidebar back link" do - visit "/" - expect(header).to have_icon_in_bot_button(icon: "robot") - header.click_bot_button - expect(ai_pm_homepage).to have_homepage - expect(header).to have_icon_in_bot_button(icon: "robot") - expect(ai_pm_homepage).to have_no_sidebar_back_link + it "renders back to forum link" do + ai_pm_homepage.visit + expect(ai_pm_homepage).to have_sidebar_back_link + end + + context "with hamburger menu" do + before { SiteSetting.navigation_menu = "header dropdown" } + it "keeps robot icon in the header and doesn't display sidebar back link" do + visit "/" + expect(header).to have_icon_in_bot_button(icon: "robot") + header.click_bot_button + expect(ai_pm_homepage).to have_homepage + expect(header).to have_icon_in_bot_button(icon: "robot") + expect(ai_pm_homepage).to have_no_sidebar_back_link + end + + it "still renders the sidebar" do + visit "/" + header.click_bot_button + expect(ai_pm_homepage).to have_homepage + expect(sidebar).to be_visible + expect(header_dropdown).to be_visible + end + end end - it "still renders the sidebar" do - visit "/" - header.click_bot_button - expect(ai_pm_homepage).to have_homepage - expect(sidebar).to be_visible - expect(header_dropdown).to be_visible + context "when `ai_enable_experimental_bot_ux` is disabled" do + before { SiteSetting.ai_enable_experimental_bot_ux = false } + + it "opens composer on bot click" do + visit "/" + header.click_bot_button + + expect(ai_pm_homepage).to have_no_homepage + expect(composer).to be_opened + end + + it "does not render sidebar when navigation menu is set to header on pm" do + SiteSetting.navigation_menu = "header dropdown" + topic_page.visit_topic(pm) + + expect(ai_pm_homepage).to have_no_homepage + expect(sidebar).to be_not_visible + expect(header_dropdown).to be_visible + end + + it "shows default content in the sidebar" do + topic_page.visit_topic(pm) + + expect(sidebar).to have_section("categories") + expect(sidebar).to have_section("messages") + expect(sidebar).to have_section("chat-dms") + expect(sidebar).to have_no_css("button.ai-new-question-button") + end end - end - end - context "when `ai_enable_experimental_bot_ux` is disabled" do - before { SiteSetting.ai_enable_experimental_bot_ux = false } + context "with header dropdown on mobile", mobile: true do + before { SiteSetting.navigation_menu = "header dropdown" } - it "opens composer on bot click" do - visit "/" - header.click_bot_button + it "displays the new question button in the menu when viewing a PM" do + ai_pm_homepage.visit + header_dropdown.open + expect(ai_pm_homepage).to have_no_new_question_button - expect(ai_pm_homepage).to have_no_homepage - expect(composer).to be_opened - end + topic_page.visit_topic(pm) + header_dropdown.open + ai_pm_homepage.click_new_question_button - it "does not render sidebar when navigation menu is set to header on pm" do - SiteSetting.navigation_menu = "header dropdown" - topic_page.visit_topic(pm) - - expect(ai_pm_homepage).to have_no_homepage - expect(sidebar).to be_not_visible - expect(header_dropdown).to be_visible - end - - it "shows default content in the sidebar" do - topic_page.visit_topic(pm) - - expect(sidebar).to have_section("categories") - expect(sidebar).to have_section("messages") - expect(sidebar).to have_section("chat-dms") - expect(sidebar).to have_no_css("button.ai-new-question-button") - end - end - - context "with header dropdown on mobile", mobile: true do - before { SiteSetting.navigation_menu = "header dropdown" } - - it "displays the new question button in the menu when viewing a PM" do - ai_pm_homepage.visit - header_dropdown.open - expect(ai_pm_homepage).to have_no_new_question_button - - topic_page.visit_topic(pm) - header_dropdown.open - ai_pm_homepage.click_new_question_button - - # Hamburger sidebar is closed - expect(header_dropdown).to have_no_dropdown_visible + # Hamburger sidebar is closed + expect(header_dropdown).to have_no_dropdown_visible + end + end end end end