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 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"}}
</span>
<span class="ai-search-discoveries-tooltip">
<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>
<AiSearchDiscoveriesTooltip />
</h3>
<div class="full-page-discoveries">
<AiSearchDiscoveries @searchTerm={{@outletArgs.search}} />

View File

@ -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"}}
</span>
<span class="ai-search-discoveries-tooltip">
<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>
<AiSearchDiscoveriesTooltip />
</h3>
<AiSearchDiscoveries @discoveryPreviewLength={{50}} />

View File

@ -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()) {

View File

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

View File

@ -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}}
<label class="control-label">{{i18n "discourse_ai.title"}}</label>
<div class="ai-user-preferences">
<legend class="control-label">{{i18n "discourse_ai.title"}}</legend>
<div class="control-group ai-setting">
<PreferenceCheckbox
@labelKey="discourse_ai.ai_helper.image_caption.automatic_caption_setting"
@checked={{this.model.user_option.auto_image_caption}}
data-setting-name="auto-image-caption"
class="pref-auto-image-caption"
{{#each this.booleanSettings as |setting|}}
{{#if setting.isIncluded}}
<div class="control-group ai-setting">
<PreferenceCheckbox
@labelKey={{setting.label}}
@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>
<SaveControls
@id="user_ai_preference_save"
@model={{this.model}}
@action={{this.save}}
@saved={{this.saved}}
/>
{{/if}}
{{/unless}}
</div>

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 {
&__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;
}

View File

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

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)
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,

View File

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

View File

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

View File

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