From bb32d0d737224e8d65c4d57b35fc27e38a3281d2 Mon Sep 17 00:00:00 2001 From: Keegan George Date: Mon, 10 Mar 2025 14:17:58 -0700 Subject: [PATCH] FEATURE: Add ability to disable search discoveries (#1177) This update adds the ability to disable search discoveries. This can be done through a tooltip when search discoveries are shown. It can also be done in the AI user preferences, which has also been updated to accommodate more than just the one image caption setting. --- .../ai-search-discoveries-tooltip.gjs | 49 +++++++++++++++++ .../ai-full-page-discobot-discoveries.gjs | 29 ++-------- .../ai-discobot-discoveries.gjs | 29 ++-------- .../discourse/controllers/preferences-ai.js | 55 +++++++++++++++---- .../services/discobot-discoveries.js | 12 +++- .../discourse/templates/preferences/ai.hbs | 49 +++++++++-------- .../initializers/ai-search-discoveries.js | 15 +++++ .../stylesheets/common/ai-user-settings.scss | 13 +++++ .../common/ai-discobot-discoveries.scss | 15 +++++ config/locales/client.en.yml | 8 ++- ...d_ai_search_discoveries_to_user_options.rb | 7 +++ lib/ai_bot/entry_point.rb | 19 +++++++ plugin.rb | 1 + spec/lib/modules/ai_bot/entry_point_spec.rb | 9 +++ spec/models/user_option_spec.rb | 20 +++++++ 15 files changed, 245 insertions(+), 85 deletions(-) create mode 100644 assets/javascripts/discourse/components/ai-search-discoveries-tooltip.gjs create mode 100644 assets/javascripts/initializers/ai-search-discoveries.js create mode 100644 assets/stylesheets/common/ai-user-settings.scss create mode 100644 db/migrate/20250310172527_add_ai_search_discoveries_to_user_options.rb diff --git a/assets/javascripts/discourse/components/ai-search-discoveries-tooltip.gjs b/assets/javascripts/discourse/components/ai-search-discoveries-tooltip.gjs new file mode 100644 index 00000000..9e881ae2 --- /dev/null +++ b/assets/javascripts/discourse/components/ai-search-discoveries-tooltip.gjs @@ -0,0 +1,49 @@ +import Component from "@glimmer/component"; +import { service } from "@ember/service"; +import DButton from "discourse/components/d-button"; +import icon from "discourse/helpers/d-icon"; +import { i18n } from "discourse-i18n"; +import DTooltip from "float-kit/components/d-tooltip"; + +export default class AiSearchDiscoveriesTooltip extends Component { + @service discobotDiscoveries; + + +} diff --git a/assets/javascripts/discourse/connectors/full-page-search-below-search-header/ai-full-page-discobot-discoveries.gjs b/assets/javascripts/discourse/connectors/full-page-search-below-search-header/ai-full-page-discobot-discoveries.gjs index ceacbf59..6fc56ad2 100644 --- a/assets/javascripts/discourse/connectors/full-page-search-below-search-header/ai-full-page-discobot-discoveries.gjs +++ b/assets/javascripts/discourse/connectors/full-page-search-below-search-header/ai-full-page-discobot-discoveries.gjs @@ -2,14 +2,15 @@ import Component from "@glimmer/component"; import { service } from "@ember/service"; import icon from "discourse/helpers/d-icon"; import { i18n } from "discourse-i18n"; -import DTooltip from "float-kit/components/d-tooltip"; import AiSearchDiscoveries from "../../components/ai-search-discoveries"; +import AiSearchDiscoveriesTooltip from "../../components/ai-search-discoveries-tooltip"; export default class AiFullPageDiscobotDiscoveries extends Component { static shouldRender(_args, { siteSettings, currentUser }) { return ( siteSettings.ai_bot_discover_persona && - currentUser?.can_use_ai_bot_discover_persona + currentUser?.can_use_ai_bot_discover_persona && + currentUser?.user_option?.ai_search_discoveries ); } @@ -29,29 +30,7 @@ export default class AiFullPageDiscobotDiscoveries extends Component { {{i18n "discourse_ai.discobot_discoveries.main_title"}} - - - <:trigger> - {{icon "circle-info"}} - - <:content> -
-
- {{i18n "discourse_ai.discobot_discoveries.tooltip.header"}} -
- -
- {{#if this.discobotDiscoveries.modelUsed}} - {{i18n - "discourse_ai.discobot_discoveries.tooltip.content" - model=this.discobotDiscoveries.modelUsed - }} - {{/if}} -
-
- -
-
+
diff --git a/assets/javascripts/discourse/connectors/search-menu-results-type-top/ai-discobot-discoveries.gjs b/assets/javascripts/discourse/connectors/search-menu-results-type-top/ai-discobot-discoveries.gjs index a63ec2ba..b23c5f03 100644 --- a/assets/javascripts/discourse/connectors/search-menu-results-type-top/ai-discobot-discoveries.gjs +++ b/assets/javascripts/discourse/connectors/search-menu-results-type-top/ai-discobot-discoveries.gjs @@ -2,15 +2,16 @@ import Component from "@glimmer/component"; import { service } from "@ember/service"; import icon from "discourse/helpers/d-icon"; import { i18n } from "discourse-i18n"; -import DTooltip from "float-kit/components/d-tooltip"; import AiSearchDiscoveries from "../../components/ai-search-discoveries"; +import AiSearchDiscoveriesTooltip from "../../components/ai-search-discoveries-tooltip"; export default class AiDiscobotDiscoveries extends Component { static shouldRender(args, { siteSettings, currentUser }) { return ( args.resultType.type === "topic" && siteSettings.ai_bot_discover_persona && - currentUser?.can_use_ai_bot_discover_persona + currentUser?.can_use_ai_bot_discover_persona && + currentUser?.user_option?.ai_search_discoveries ); } @@ -24,29 +25,7 @@ export default class AiDiscobotDiscoveries extends Component { {{i18n "discourse_ai.discobot_discoveries.main_title"}} - - - <:trigger> - {{icon "circle-info"}} - - <:content> -
-
- {{i18n "discourse_ai.discobot_discoveries.tooltip.header"}} -
- -
- {{#if this.discobotDiscoveries.modelUsed}} - {{i18n - "discourse_ai.discobot_discoveries.tooltip.content" - model=this.discobotDiscoveries.modelUsed - }} - {{/if}} -
-
- -
-
+ diff --git a/assets/javascripts/discourse/controllers/preferences-ai.js b/assets/javascripts/discourse/controllers/preferences-ai.js index 63eeb9c6..dab01d5d 100644 --- a/assets/javascripts/discourse/controllers/preferences-ai.js +++ b/assets/javascripts/discourse/controllers/preferences-ai.js @@ -5,21 +5,54 @@ import { service } from "@ember/service"; import { popupAjaxError } from "discourse/lib/ajax-error"; import { isTesting } from "discourse/lib/environment"; -const AI_ATTRS = ["auto_image_caption"]; - export default class PreferencesAiController extends Controller { @service siteSettings; @tracked saved = false; - get showAutoImageCaptionSetting() { - const aiHelperEnabledFeatures = - this.siteSettings.ai_helper_enabled_features.split("|"); + get booleanSettings() { + return [ + { + key: "auto_image_caption", + label: "discourse_ai.ai_helper.image_caption.automatic_caption_setting", + settingName: "auto-image-caption", + checked: this.model.user_option.auto_image_caption, + isIncluded: (() => { + const aiHelperEnabledFeatures = + this.siteSettings.ai_helper_enabled_features.split("|"); - return ( - this.model?.user_allowed_ai_auto_image_captions && - aiHelperEnabledFeatures.includes("image_caption") && - this.siteSettings.ai_helper_enabled - ); + return ( + this.model?.user_allowed_ai_auto_image_captions && + aiHelperEnabledFeatures.includes("image_caption") && + this.siteSettings.ai_helper_enabled + ); + })(), + }, + { + key: "ai_search_discoveries", + label: "discourse_ai.discobot_discoveries.user_setting", + settingName: "ai-search-discoveries", + checked: this.model.user_option.ai_search_discoveries, + isIncluded: (() => { + return ( + this.siteSettings.ai_bot_discover_persona && + this.model?.can_use_ai_bot_discover_persona && + this.siteSettings.ai_bot_enabled + ); + })(), + }, + ]; + } + + get userSettingAttributes() { + const attrs = []; + + this.booleanSettings.forEach((setting) => { + if (setting.isIncluded) { + attrs.push(setting.key); + } + }); + + return attrs; } @action @@ -27,7 +60,7 @@ export default class PreferencesAiController extends Controller { this.saved = false; return this.model - .save(AI_ATTRS) + .save(this.userSettingAttributes) .then(() => { this.saved = true; if (!isTesting()) { diff --git a/assets/javascripts/discourse/services/discobot-discoveries.js b/assets/javascripts/discourse/services/discobot-discoveries.js index ad77939b..f7618689 100644 --- a/assets/javascripts/discourse/services/discobot-discoveries.js +++ b/assets/javascripts/discourse/services/discobot-discoveries.js @@ -1,9 +1,12 @@ import { tracked } from "@glimmer/tracking"; -import Service from "@ember/service"; +import { action } from "@ember/object"; +import Service, { service } from "@ember/service"; export default class DiscobotDiscoveries extends Service { // We use this to retain state after search menu gets closed. // Similar to discourse/discourse#25504 + @service currentUser; + @tracked discovery = ""; @tracked lastQuery = ""; @tracked discoveryTimedOut = false; @@ -14,4 +17,11 @@ export default class DiscobotDiscoveries extends Service { this.discoveryTimedOut = false; this.modelUsed = ""; } + + @action + async disableDiscoveries() { + this.currentUser.user_option.ai_search_discoveries = false; + await this.currentUser.save(["ai_search_discoveries"]); + location.reload(); + } } diff --git a/assets/javascripts/discourse/templates/preferences/ai.hbs b/assets/javascripts/discourse/templates/preferences/ai.hbs index 182a2f73..0f22ac26 100644 --- a/assets/javascripts/discourse/templates/preferences/ai.hbs +++ b/assets/javascripts/discourse/templates/preferences/ai.hbs @@ -1,24 +1,29 @@ -{{! - Later when we have more preferences, - move the conditional (showAutoImageCaptionSetting) - to be only around the auto-image-caption preference. - }} -{{#if this.showAutoImageCaptionSetting}} - +
+ {{i18n "discourse_ai.title"}} -
- + +
+ {{/if}} + {{/each}} + + {{#if (eq this.userSettingAttributes.length 0)}} + {{i18n "discourse_ai.user_preferences.empty"}} + {{/if}} + + {{#unless (eq this.userSettingAttributes.length 0)}} + -
- - -{{/if}} \ No newline at end of file + {{/unless}} +
\ No newline at end of file diff --git a/assets/javascripts/initializers/ai-search-discoveries.js b/assets/javascripts/initializers/ai-search-discoveries.js new file mode 100644 index 00000000..339cb944 --- /dev/null +++ b/assets/javascripts/initializers/ai-search-discoveries.js @@ -0,0 +1,15 @@ +import { apiInitializer } from "discourse/lib/api"; + +export default apiInitializer((api) => { + const currentUser = api.getCurrentUser(); + const settings = api.container.lookup("service:site-settings"); + + if ( + !settings.ai_bot_enabled || + !currentUser?.can_use_ai_bot_discover_persona + ) { + return; + } + + api.addSaveableUserOptionField("ai_search_discoveries"); +}); diff --git a/assets/stylesheets/common/ai-user-settings.scss b/assets/stylesheets/common/ai-user-settings.scss new file mode 100644 index 00000000..95fd6a1e --- /dev/null +++ b/assets/stylesheets/common/ai-user-settings.scss @@ -0,0 +1,13 @@ +.user-preferences .ai-user-preferences { + legend { + margin-bottom: 1rem; + } + + .control-group { + margin-bottom: 0; + } + + .save-button { + margin-top: 2rem; + } +} diff --git a/assets/stylesheets/modules/ai-bot/common/ai-discobot-discoveries.scss b/assets/stylesheets/modules/ai-bot/common/ai-discobot-discoveries.scss index fb2ff76a..83a6e795 100644 --- a/assets/stylesheets/modules/ai-bot/common/ai-discobot-discoveries.scss +++ b/assets/stylesheets/modules/ai-bot/common/ai-discobot-discoveries.scss @@ -63,11 +63,26 @@ } .ai-search-discoveries-tooltip { + &__content { + padding: 0.5rem; + } + &__header { font-weight: bold; margin-bottom: 0.5em; } + &__actions { + display: flex; + justify-content: space-between; + gap: 1rem; + margin-top: 1rem; + + .btn { + padding: 0; + } + } + .fk-d-tooltip__trigger { vertical-align: middle; } diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index a2d5f2a3..a6272d0c 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -99,7 +99,6 @@ en: label: "Tool" description: "Tool to use for triage (tool must have no parameters defined)" - llm_persona_triage: fields: persona: @@ -714,9 +713,16 @@ en: tell_me_more: "Tell me more..." collapse: "Collapse" timed_out: "Discobot couldn't find any discoveries. Something went wrong." + user_setting: "Enable search discoveries" tooltip: header: "AI powered search" content: "Natural language search powered by %{model}" + actions: + info: "How does it work?" + disable: "Disable" + + user_preferences: + empty: "There are no relevant settings available at this time" review: types: reviewable_ai_post: diff --git a/db/migrate/20250310172527_add_ai_search_discoveries_to_user_options.rb b/db/migrate/20250310172527_add_ai_search_discoveries_to_user_options.rb new file mode 100644 index 00000000..44620c96 --- /dev/null +++ b/db/migrate/20250310172527_add_ai_search_discoveries_to_user_options.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddAiSearchDiscoveriesToUserOptions < ActiveRecord::Migration[7.2] + def change + add_column :user_options, :ai_search_discoveries, :boolean, default: true, null: false + end +end diff --git a/lib/ai_bot/entry_point.rb b/lib/ai_bot/entry_point.rb index cb815b7b..70d4bac5 100644 --- a/lib/ai_bot/entry_point.rb +++ b/lib/ai_bot/entry_point.rb @@ -180,6 +180,25 @@ module DiscourseAi scope.user.in_any_groups?(persona_allowed_groups) end + UserUpdater::OPTION_ATTR.push(:ai_search_discoveries) + plugin.add_to_serializer( + :user_option, + :ai_search_discoveries, + include_condition: -> do + SiteSetting.ai_bot_enabled && SiteSetting.ai_bot_discover_persona.present? && + scope.authenticated? + end, + ) { object.ai_search_discoveries } + + plugin.add_to_serializer( + :current_user_option, + :ai_search_discoveries, + include_condition: -> do + SiteSetting.ai_bot_enabled && SiteSetting.ai_bot_discover_persona.present? && + scope.authenticated? + end, + ) { object.ai_search_discoveries } + plugin.add_to_serializer( :topic_view, :ai_persona_name, diff --git a/plugin.rb b/plugin.rb index e3218d93..7f6e1389 100644 --- a/plugin.rb +++ b/plugin.rb @@ -26,6 +26,7 @@ enabled_site_setting :discourse_ai_enabled register_asset "stylesheets/common/streaming.scss" register_asset "stylesheets/common/ai-blinking-animation.scss" +register_asset "stylesheets/common/ai-user-settings.scss" register_asset "stylesheets/modules/ai-helper/common/ai-helper.scss" register_asset "stylesheets/modules/ai-helper/desktop/ai-helper-fk-modals.scss", :desktop diff --git a/spec/lib/modules/ai_bot/entry_point_spec.rb b/spec/lib/modules/ai_bot/entry_point_spec.rb index 57da71c3..447a5ac5 100644 --- a/spec/lib/modules/ai_bot/entry_point_spec.rb +++ b/spec/lib/modules/ai_bot/entry_point_spec.rb @@ -161,5 +161,14 @@ RSpec.describe DiscourseAi::AiBot::EntryPoint do end end end + + it "will include ai_search_discoveries field in the user_option if discover persona is enabled" do + SiteSetting.ai_bot_enabled = true + SiteSetting.ai_bot_discover_persona = Fabricate(:ai_persona).id + + serializer = + CurrentUserSerializer.new(Fabricate(:user), scope: Guardian.new(Fabricate(:user))) + expect(serializer.user_option.ai_search_discoveries).to eq(true) + end end end diff --git a/spec/models/user_option_spec.rb b/spec/models/user_option_spec.rb index 54c58b8f..34121ab9 100644 --- a/spec/models/user_option_spec.rb +++ b/spec/models/user_option_spec.rb @@ -1,12 +1,21 @@ # frozen_string_literal: true RSpec.describe UserOption do + fab!(:user) + fab!(:llm_model) + fab!(:group) + fab!(:ai_persona) do + Fabricate(:ai_persona, allowed_group_ids: [group.id], default_llm_id: llm_model.id) + end + before do assign_fake_provider_to(:ai_helper_model) assign_fake_provider_to(:ai_helper_image_caption_model) SiteSetting.ai_helper_enabled = true SiteSetting.ai_helper_enabled_features = "image_caption" SiteSetting.ai_auto_image_caption_allowed_groups = "10" # tl0 + + SiteSetting.ai_bot_enabled = true end describe "#auto_image_caption" do @@ -14,4 +23,15 @@ RSpec.describe UserOption do expect(described_class.new.auto_image_caption).to eq(false) end end + + describe "#ai_search_discoveries" do + before do + SiteSetting.ai_bot_discover_persona = ai_persona.id + group.add(user) + end + + it "is present" do + expect(described_class.new.ai_search_discoveries).to eq(true) + end + end end