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;
+
+
+
+
+ <:trigger>
+ {{icon "circle-info"}}
+
+ <:content>
+
+
+
+
+ {{#if this.discobotDiscoveries.modelUsed}}
+ {{i18n
+ "discourse_ai.discobot_discoveries.tooltip.content"
+ model=this.discobotDiscoveries.modelUsed
+ }}
+ {{/if}}
+
+
+
+
+
+
+
+
+
+
+
+}
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>
-
-
-
-
- {{#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>
-
-
-
-
- {{#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}}
-
+
+
-
+ {{/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