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