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.
This commit is contained in:
Keegan George 2025-03-10 14:17:58 -07:00 committed by GitHub
parent 339251a371
commit bb32d0d737
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 245 additions and 85 deletions

View File

@ -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;
<template>
<span class="ai-search-discoveries-tooltip">
<DTooltip @placement="top-end" @interactive={{true}}>
<:trigger>
{{icon "circle-info"}}
</:trigger>
<:content>
<div class="ai-search-discoveries-tooltip__content">
<div class="ai-search-discoveries-tooltip__header">
{{i18n "discourse_ai.discobot_discoveries.tooltip.header"}}
</div>
<div class="ai-search-discoveries-tooltip__description">
{{#if this.discobotDiscoveries.modelUsed}}
{{i18n
"discourse_ai.discobot_discoveries.tooltip.content"
model=this.discobotDiscoveries.modelUsed
}}
{{/if}}
</div>
<div class="ai-search-discoveries-tooltip__actions">
<DButton
class="btn-transparent btn-primary"
@label="discourse_ai.discobot_discoveries.tooltip.actions.info"
@href="https://meta.discourse.org/t/conversational-ai-search-coming-to-discourse-ai/355939"
/>
<DButton
class="btn-transparent btn-danger"
@label="discourse_ai.discobot_discoveries.tooltip.actions.disable"
@action={{this.discobotDiscoveries.disableDiscoveries}}
/>
</div>
</div>
</:content>
</DTooltip>
</span>
</template>
}

View File

@ -2,14 +2,15 @@ import Component from "@glimmer/component";
import { service } from "@ember/service"; import { service } from "@ember/service";
import icon from "discourse/helpers/d-icon"; import icon from "discourse/helpers/d-icon";
import { i18n } from "discourse-i18n"; import { i18n } from "discourse-i18n";
import DTooltip from "float-kit/components/d-tooltip";
import AiSearchDiscoveries from "../../components/ai-search-discoveries"; import AiSearchDiscoveries from "../../components/ai-search-discoveries";
import AiSearchDiscoveriesTooltip from "../../components/ai-search-discoveries-tooltip";
export default class AiFullPageDiscobotDiscoveries extends Component { export default class AiFullPageDiscobotDiscoveries extends Component {
static shouldRender(_args, { siteSettings, currentUser }) { static shouldRender(_args, { siteSettings, currentUser }) {
return ( return (
siteSettings.ai_bot_discover_persona && 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"}} {{i18n "discourse_ai.discobot_discoveries.main_title"}}
</span> </span>
<span class="ai-search-discoveries-tooltip"> <AiSearchDiscoveriesTooltip />
<DTooltip @placement="top-end">
<:trigger>
{{icon "circle-info"}}
</:trigger>
<:content>
<div class="ai-search-discoveries-tooltip__content">
<div class="ai-search-discoveries-tooltip__header">
{{i18n "discourse_ai.discobot_discoveries.tooltip.header"}}
</div>
<div class="ai-search-discoveries-tooltip__content">
{{#if this.discobotDiscoveries.modelUsed}}
{{i18n
"discourse_ai.discobot_discoveries.tooltip.content"
model=this.discobotDiscoveries.modelUsed
}}
{{/if}}
</div>
</div>
</:content>
</DTooltip>
</span>
</h3> </h3>
<div class="full-page-discoveries"> <div class="full-page-discoveries">
<AiSearchDiscoveries @searchTerm={{@outletArgs.search}} /> <AiSearchDiscoveries @searchTerm={{@outletArgs.search}} />

View File

@ -2,15 +2,16 @@ import Component from "@glimmer/component";
import { service } from "@ember/service"; import { service } from "@ember/service";
import icon from "discourse/helpers/d-icon"; import icon from "discourse/helpers/d-icon";
import { i18n } from "discourse-i18n"; import { i18n } from "discourse-i18n";
import DTooltip from "float-kit/components/d-tooltip";
import AiSearchDiscoveries from "../../components/ai-search-discoveries"; import AiSearchDiscoveries from "../../components/ai-search-discoveries";
import AiSearchDiscoveriesTooltip from "../../components/ai-search-discoveries-tooltip";
export default class AiDiscobotDiscoveries extends Component { export default class AiDiscobotDiscoveries extends Component {
static shouldRender(args, { siteSettings, currentUser }) { static shouldRender(args, { siteSettings, currentUser }) {
return ( return (
args.resultType.type === "topic" && args.resultType.type === "topic" &&
siteSettings.ai_bot_discover_persona && 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"}} {{i18n "discourse_ai.discobot_discoveries.main_title"}}
</span> </span>
<span class="ai-search-discoveries-tooltip"> <AiSearchDiscoveriesTooltip />
<DTooltip @placement="top-end">
<:trigger>
{{icon "circle-info"}}
</:trigger>
<:content>
<div class="ai-search-discoveries-tooltip__content">
<div class="ai-search-discoveries-tooltip__header">
{{i18n "discourse_ai.discobot_discoveries.tooltip.header"}}
</div>
<div class="ai-search-discoveries-tooltip__content">
{{#if this.discobotDiscoveries.modelUsed}}
{{i18n
"discourse_ai.discobot_discoveries.tooltip.content"
model=this.discobotDiscoveries.modelUsed
}}
{{/if}}
</div>
</div>
</:content>
</DTooltip>
</span>
</h3> </h3>
<AiSearchDiscoveries @discoveryPreviewLength={{50}} /> <AiSearchDiscoveries @discoveryPreviewLength={{50}} />

View File

@ -5,21 +5,54 @@ import { service } from "@ember/service";
import { popupAjaxError } from "discourse/lib/ajax-error"; import { popupAjaxError } from "discourse/lib/ajax-error";
import { isTesting } from "discourse/lib/environment"; import { isTesting } from "discourse/lib/environment";
const AI_ATTRS = ["auto_image_caption"];
export default class PreferencesAiController extends Controller { export default class PreferencesAiController extends Controller {
@service siteSettings; @service siteSettings;
@tracked saved = false; @tracked saved = false;
get showAutoImageCaptionSetting() { get booleanSettings() {
const aiHelperEnabledFeatures = return [
this.siteSettings.ai_helper_enabled_features.split("|"); {
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 ( return (
this.model?.user_allowed_ai_auto_image_captions && this.model?.user_allowed_ai_auto_image_captions &&
aiHelperEnabledFeatures.includes("image_caption") && aiHelperEnabledFeatures.includes("image_caption") &&
this.siteSettings.ai_helper_enabled 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 @action
@ -27,7 +60,7 @@ export default class PreferencesAiController extends Controller {
this.saved = false; this.saved = false;
return this.model return this.model
.save(AI_ATTRS) .save(this.userSettingAttributes)
.then(() => { .then(() => {
this.saved = true; this.saved = true;
if (!isTesting()) { if (!isTesting()) {

View File

@ -1,9 +1,12 @@
import { tracked } from "@glimmer/tracking"; 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 { export default class DiscobotDiscoveries extends Service {
// We use this to retain state after search menu gets closed. // We use this to retain state after search menu gets closed.
// Similar to discourse/discourse#25504 // Similar to discourse/discourse#25504
@service currentUser;
@tracked discovery = ""; @tracked discovery = "";
@tracked lastQuery = ""; @tracked lastQuery = "";
@tracked discoveryTimedOut = false; @tracked discoveryTimedOut = false;
@ -14,4 +17,11 @@ export default class DiscobotDiscoveries extends Service {
this.discoveryTimedOut = false; this.discoveryTimedOut = false;
this.modelUsed = ""; this.modelUsed = "";
} }
@action
async disableDiscoveries() {
this.currentUser.user_option.ai_search_discoveries = false;
await this.currentUser.save(["ai_search_discoveries"]);
location.reload();
}
} }

View File

@ -1,24 +1,29 @@
{{! <div class="ai-user-preferences">
Later when we have more preferences, <legend class="control-label">{{i18n "discourse_ai.title"}}</legend>
move the conditional (showAutoImageCaptionSetting)
to be only around the auto-image-caption preference.
}}
{{#if this.showAutoImageCaptionSetting}}
<label class="control-label">{{i18n "discourse_ai.title"}}</label>
<div class="control-group ai-setting"> {{#each this.booleanSettings as |setting|}}
<PreferenceCheckbox {{#if setting.isIncluded}}
@labelKey="discourse_ai.ai_helper.image_caption.automatic_caption_setting" <div class="control-group ai-setting">
@checked={{this.model.user_option.auto_image_caption}} <PreferenceCheckbox
data-setting-name="auto-image-caption" @labelKey={{setting.label}}
class="pref-auto-image-caption" @checked={{get this.model.user_option setting.key}}
data-setting-name={{setting.settingName}}
class="pref-{{setting.settingName}}"
/>
</div>
{{/if}}
{{/each}}
{{#if (eq this.userSettingAttributes.length 0)}}
{{i18n "discourse_ai.user_preferences.empty"}}
{{/if}}
{{#unless (eq this.userSettingAttributes.length 0)}}
<SaveControls
@id="user_ai_preference_save"
@model={{this.model}}
@action={{this.save}}
@saved={{this.saved}}
/> />
</div> {{/unless}}
</div>
<SaveControls
@id="user_ai_preference_save"
@model={{this.model}}
@action={{this.save}}
@saved={{this.saved}}
/>
{{/if}}

View File

@ -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");
});

View File

@ -0,0 +1,13 @@
.user-preferences .ai-user-preferences {
legend {
margin-bottom: 1rem;
}
.control-group {
margin-bottom: 0;
}
.save-button {
margin-top: 2rem;
}
}

View File

@ -63,11 +63,26 @@
} }
.ai-search-discoveries-tooltip { .ai-search-discoveries-tooltip {
&__content {
padding: 0.5rem;
}
&__header { &__header {
font-weight: bold; font-weight: bold;
margin-bottom: 0.5em; margin-bottom: 0.5em;
} }
&__actions {
display: flex;
justify-content: space-between;
gap: 1rem;
margin-top: 1rem;
.btn {
padding: 0;
}
}
.fk-d-tooltip__trigger { .fk-d-tooltip__trigger {
vertical-align: middle; vertical-align: middle;
} }

View File

@ -99,7 +99,6 @@ en:
label: "Tool" label: "Tool"
description: "Tool to use for triage (tool must have no parameters defined)" description: "Tool to use for triage (tool must have no parameters defined)"
llm_persona_triage: llm_persona_triage:
fields: fields:
persona: persona:
@ -714,9 +713,16 @@ en:
tell_me_more: "Tell me more..." tell_me_more: "Tell me more..."
collapse: "Collapse" collapse: "Collapse"
timed_out: "Discobot couldn't find any discoveries. Something went wrong." timed_out: "Discobot couldn't find any discoveries. Something went wrong."
user_setting: "Enable search discoveries"
tooltip: tooltip:
header: "AI powered search" header: "AI powered search"
content: "Natural language search powered by %{model}" 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: review:
types: types:
reviewable_ai_post: reviewable_ai_post:

View File

@ -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

View File

@ -180,6 +180,25 @@ module DiscourseAi
scope.user.in_any_groups?(persona_allowed_groups) scope.user.in_any_groups?(persona_allowed_groups)
end 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( plugin.add_to_serializer(
:topic_view, :topic_view,
:ai_persona_name, :ai_persona_name,

View File

@ -26,6 +26,7 @@ enabled_site_setting :discourse_ai_enabled
register_asset "stylesheets/common/streaming.scss" register_asset "stylesheets/common/streaming.scss"
register_asset "stylesheets/common/ai-blinking-animation.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/common/ai-helper.scss"
register_asset "stylesheets/modules/ai-helper/desktop/ai-helper-fk-modals.scss", :desktop register_asset "stylesheets/modules/ai-helper/desktop/ai-helper-fk-modals.scss", :desktop

View File

@ -161,5 +161,14 @@ RSpec.describe DiscourseAi::AiBot::EntryPoint do
end end
end 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
end end

View File

@ -1,12 +1,21 @@
# frozen_string_literal: true # frozen_string_literal: true
RSpec.describe UserOption do 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 before do
assign_fake_provider_to(:ai_helper_model) assign_fake_provider_to(:ai_helper_model)
assign_fake_provider_to(:ai_helper_image_caption_model) assign_fake_provider_to(:ai_helper_image_caption_model)
SiteSetting.ai_helper_enabled = true SiteSetting.ai_helper_enabled = true
SiteSetting.ai_helper_enabled_features = "image_caption" SiteSetting.ai_helper_enabled_features = "image_caption"
SiteSetting.ai_auto_image_caption_allowed_groups = "10" # tl0 SiteSetting.ai_auto_image_caption_allowed_groups = "10" # tl0
SiteSetting.ai_bot_enabled = true
end end
describe "#auto_image_caption" do describe "#auto_image_caption" do
@ -14,4 +23,15 @@ RSpec.describe UserOption do
expect(described_class.new.auto_image_caption).to eq(false) expect(described_class.new.auto_image_caption).to eq(false)
end end
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 end