Skip to content
This repository was archived by the owner on Jul 22, 2025. It is now read-only.

Commit 241e565

Browse files
committed
Improves entire flow for first time usage and adds a system
spec
1 parent eb3a692 commit 241e565

File tree

7 files changed

+106
-13
lines changed

7 files changed

+106
-13
lines changed

app/controllers/discourse_ai/admin/ai_spam_controller.rb

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,23 @@ def update
2121
} if allowed_params.key?(:custom_instructions)
2222

2323
if updated_params.present?
24-
updated_params[:setting_type] = :spam
25-
AiModerationSetting.upsert(updated_params, unique_by: :setting_type)
24+
# not using upsert cause we will not get the correct validation errors
25+
if AiModerationSetting.spam
26+
AiModerationSetting.spam.update!(updated_params)
27+
else
28+
AiModerationSetting.create!(updated_params.merge(setting_type: :spam))
29+
end
2630
end
2731

2832
is_enabled = ActiveModel::Type::Boolean.new.cast(allowed_params[:is_enabled])
2933

30-
SiteSetting.ai_spam_detection_enabled = is_enabled if allowed_params.key?(:is_enabled)
34+
if allowed_params.key?(:is_enabled)
35+
if is_enabled && !AiModerationSetting.spam&.llm_model_id
36+
return render_json_error(I18n.t("discourse_ai.llm.configuration.must_select_model"), status: 422)
37+
end
38+
39+
SiteSetting.ai_spam_detection_enabled = is_enabled
40+
end
3141

3242
render json: AiSpamSerializer.new(spam_config, root: false)
3343
end

app/models/ai_moderation_setting.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
class AiModerationSetting < ActiveRecord::Base
33
belongs_to :llm_model
44

5+
validates :llm_model_id, presence: true
56
validates :setting_type, presence: true
67
validates :setting_type, uniqueness: true
78

assets/javascripts/discourse/components/ai-spam.gjs

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { tracked } from "@glimmer/tracking";
33
import { fn } from "@ember/helper";
44
import { on } from "@ember/modifier";
55
import { action } from "@ember/object";
6+
import { LinkTo } from "@ember/routing";
67
import { service } from "@ember/service";
78
import DButton from "discourse/components/d-button";
89
import DToggleSwitch from "discourse/components/d-toggle-switch";
@@ -38,7 +39,15 @@ export default class AiSpam extends Component {
3839
initializeFromModel() {
3940
const model = this.args.model;
4041
this.isEnabled = model.is_enabled;
41-
this.selectedLLM = "custom:" + model.llm_id;
42+
43+
if (model.llm_id) {
44+
this.selectedLLM = "custom:" + model.llm_id;
45+
} else {
46+
if (this.availableLLMs.length) {
47+
this.selectedLLM = this.availableLLMs[0].id;
48+
this.autoSelectedLLM = true;
49+
}
50+
}
4251
this.customInstructions = model.custom_instructions;
4352
this.stats = model.stats;
4453
}
@@ -49,15 +58,20 @@ export default class AiSpam extends Component {
4958

5059
@action
5160
async toggleEnabled() {
61+
this.isEnabled = !this.isEnabled;
62+
const data = { is_enabled: this.isEnabled };
63+
if (this.autoSelectedLLM) {
64+
data.llm_model_id = this.selectedLLM.toString().split(":")[1];
65+
}
5266
try {
53-
// so UI responds immediately
54-
this.isEnabled = !this.isEnabled;
5567
const response = await ajax("/admin/plugins/discourse-ai/ai-spam.json", {
5668
type: "PUT",
57-
data: { is_enabled: this.isEnabled },
69+
data,
5870
});
71+
this.autoSelectedLLM = false;
5972
this.isEnabled = response.is_enabled;
6073
} catch (error) {
74+
this.isEnabled = !this.isEnabled;
6175
popupAjaxError(error);
6276
}
6377
}
@@ -132,12 +146,20 @@ export default class AiSpam extends Component {
132146
<label class="ai-spam__llm-label">{{i18n
133147
"discourse_ai.spam.select_llm"
134148
}}</label>
135-
<ComboBox
136-
@value={{this.selectedLLM}}
137-
@content={{this.availableLLMs}}
138-
@onChange={{this.updateLLM}}
139-
class="ai-spam__llm-selector"
140-
/>
149+
{{#if this.availableLLMs.length}}
150+
<ComboBox
151+
@value={{this.selectedLLM}}
152+
@content={{this.availableLLMs}}
153+
@onChange={{this.updateLLM}}
154+
class="ai-spam__llm-selector"
155+
/>
156+
{{else}}
157+
<span class="ai-spam__llm-placeholder">
158+
<LinkTo @route="adminPlugins.show.discourse-ai-llms.index">
159+
{{i18n "discourse_ai.spam.no_llms"}}
160+
</LinkTo>
161+
</span>
162+
{{/if}}
141163
</div>
142164

143165
<div class="ai-spam__instructions">

config/locales/client.en.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ en:
141141
enable: "Enable"
142142
spam_tip: "AI spam detection will scan the first 3 posts by all new users on public topics. It will flag them for review and block users if they are likely spam."
143143
settings_saved: "Settings saved"
144+
no_llms: "No LLMs available"
144145

145146
usage:
146147
short_title: "Usage"

config/locales/server.en.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,7 @@ en:
419419
set_llm_first: "Set %{setting} first."
420420
model_unreachable: "We couldn't get a response from this model. Check your settings first."
421421
invalid_seeded_model: "You can't use this model with this feature."
422+
must_select_model: "You must select a LLM first."
422423
endpoints:
423424
not_configured: "%{display_name} (not configured)"
424425
configuration_hint:

spec/requests/admin/ai_spam_controller_spec.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@
2525
expect(AiModerationSetting.spam.data["custom_instructions"]).to eq("custom instructions")
2626
end
2727

28+
it "can not enable spam detection without a model selected" do
29+
put "/admin/plugins/discourse-ai/ai-spam.json", params: { custom_instructions: "custom instructions" }
30+
expect(response.status).to eq(422)
31+
end
32+
33+
it "can not fiddle with custom instructions without an llm" do
34+
put "/admin/plugins/discourse-ai/ai-spam.json", params: { is_enabled: true}
35+
expect(response.status).to eq(422)
36+
end
37+
2838
context "when spam detection was already set" do
2939
fab!(:setting) do
3040
AiModerationSetting.create(
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe "AI Spam Configuration", type: :system, js: true do
4+
fab!(:admin)
5+
let(:llm_model) { Fabricate(:llm_model) }
6+
7+
before do
8+
SiteSetting.discourse_ai_enabled = true
9+
sign_in(admin)
10+
end
11+
12+
it "can properly configure spam settings" do
13+
visit "/admin/plugins/discourse-ai/ai-spam"
14+
15+
expect(page).to have_css(".ai-spam__llm-placeholder")
16+
17+
toggle = PageObjects::Components::DToggleSwitch.new(".ai-spam__toggle")
18+
19+
toggle.toggle
20+
dialog = PageObjects::Components::Dialog.new
21+
expect(dialog).to have_content(I18n.t("discourse_ai.llm.configuration.must_select_model"))
22+
dialog.click_ok
23+
24+
expect(toggle.unchecked?).to eq(true)
25+
26+
llm_model
27+
visit "/admin/plugins/discourse-ai/ai-spam"
28+
29+
toggle = PageObjects::Components::DToggleSwitch.new(".ai-spam__toggle")
30+
toggle.toggle
31+
32+
try_until_success { expect(AiModerationSetting.spam&.llm_model_id).to eq(llm_model.id) }
33+
34+
find(".ai-spam__instructions-input").fill_in(with: "Test spam detection instructions")
35+
find(".ai-spam__instructions-save").click
36+
37+
toasts = PageObjects::Components::Toasts.new
38+
expect(toasts).to have_content(I18n.t("js.discourse_ai.spam.settings_saved"))
39+
40+
expect(AiModerationSetting.spam.custom_instructions).to eq("Test spam detection instructions")
41+
42+
visit "/admin/plugins/discourse-ai/ai-llms"
43+
44+
expect(find(".ai-llm-list-editor__usages")).to have_content(
45+
I18n.t("js.discourse_ai.llms.usage.ai_spam"),
46+
)
47+
end
48+
end

0 commit comments

Comments
 (0)