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

Commit e7ace88

Browse files
committed
more WIP
1 parent a5cd6fa commit e7ace88

File tree

7 files changed

+159
-37
lines changed

7 files changed

+159
-37
lines changed

app/controllers/discourse_ai/admin/ai_spam_controller.rb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,8 @@ class AiSpamController < ::Admin::AdminController
66
requires_plugin "discourse-ai"
77

88
def show
9-
render json: { work: "in progress" }
9+
render json: AiSpamSerializer.new({}, root: false)
1010
end
11-
1211
end
1312
end
1413
end

app/models/llm_model.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,11 @@ def self.provider_params
5151
end
5252

5353
def to_llm
54-
DiscourseAi::Completions::Llm.proxy("custom:#{id}")
54+
DiscourseAi::Completions::Llm.proxy(identifier)
55+
end
56+
57+
def identifier
58+
"custom:#{id}"
5559
end
5660

5761
def toggle_companion_user
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# frozen_string_literal: true
2+
3+
class AiSpamSerializer < ApplicationSerializer
4+
attributes :is_enabled, :selected_llm, :custom_instructions, :available_llms, :stats
5+
6+
def is_enabled
7+
# Read from your hidden setting
8+
SiteSetting.ai_spam_detection_enabled
9+
end
10+
11+
def selected_llm
12+
SiteSetting.ai_spam_detection_model
13+
end
14+
15+
def custom_instructions
16+
SiteSetting.ai_spam_detection_custom_instructions
17+
end
18+
19+
def available_llms
20+
DiscourseAi::Configuration::LlmEnumerator.values.map do |hash|
21+
{ id: hash[:value], name: hash[:name] }
22+
end
23+
end
24+
25+
def stats
26+
{
27+
scanned_count: 1, # Replace with actual stats
28+
spam_detected: 2,
29+
false_positives: 3,
30+
false_negatives: 4,
31+
}
32+
end
33+
end

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

Lines changed: 22 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import Component from "@glimmer/component";
22
import { tracked } from "@glimmer/tracking";
3-
import { fn, hash } from "@ember/helper";
3+
import { fn } from "@ember/helper";
44
import { on } from "@ember/modifier";
55
import { action } from "@ember/object";
66
import { service } from "@ember/service";
7-
import { not } from "truth-helpers";
87
import DButton from "discourse/components/d-button";
98
import DToggleSwitch from "discourse/components/d-toggle-switch";
109
import DTooltip from "discourse/components/d-tooltip";
@@ -26,10 +25,19 @@ export default class AiSpam extends Component {
2625
@tracked isEnabled = false;
2726
@tracked selectedLLM = null;
2827
@tracked customInstructions = "";
29-
@tracked isLoadingStats = false;
3028

3129
constructor() {
3230
super(...arguments);
31+
this.initializeFromModel();
32+
}
33+
34+
@action
35+
initializeFromModel() {
36+
const model = this.args.model;
37+
this.isEnabled = model.is_enabled;
38+
this.selectedLLM = model.selected_llm;
39+
this.customInstructions = model.custom_instructions;
40+
this.stats = model.stats;
3341
}
3442

3543
get availableLLMs() {
@@ -39,22 +47,6 @@ export default class AiSpam extends Component {
3947
}));
4048
}
4149

42-
@action
43-
async loadStats() {
44-
this.isLoadingStats = true;
45-
try {
46-
const response = await ajax("/admin/plugins/discourse-ai/ai-spam.json");
47-
this.stats = response.stats;
48-
this.isEnabled = response.is_enabled;
49-
this.selectedLLM = response.selected_llm;
50-
this.customInstructions = response.custom_instructions;
51-
} catch (error) {
52-
popupAjaxError(error);
53-
} finally {
54-
this.isLoadingStats = false;
55-
}
56-
}
57-
5850
@action
5951
async toggleEnabled() {
6052
try {
@@ -73,15 +65,7 @@ export default class AiSpam extends Component {
7365

7466
@action
7567
async updateLLM(value) {
76-
try {
77-
await ajax("/admin/plugins/discourse-ai/ai-spam/llm", {
78-
type: "PUT",
79-
data: { llm: value },
80-
});
81-
this.selectedLLM = value;
82-
} catch (error) {
83-
popupAjaxError(error);
84-
}
68+
this.selectedLLM = value;
8569
}
8670

8771
@action
@@ -103,11 +87,17 @@ export default class AiSpam extends Component {
10387
"discourse_ai.spam.title"
10488
}}</h3>
10589

106-
<div class="ai-spam__toggle">
90+
<div class="control-group ai-persona-editor__priority">
10791
<DToggleSwitch
92+
class="ai-spam__toggle"
10893
@state={{this.enabled}}
94+
@label="discourse_ai.spam.enable"
10995
{{on "click" this.toggleEnabled}}
11096
/>
97+
<DTooltip
98+
@icon="question-circle"
99+
@content={{i18n "discourse_ai.spam.spam_tip"}}
100+
/>
111101
</div>
112102

113103
<div class="ai-spam__llm">
@@ -118,7 +108,6 @@ export default class AiSpam extends Component {
118108
@value={{this.selectedLLM}}
119109
@content={{this.availableLLMs}}
120110
@onChange={{this.updateLLM}}
121-
@options={{hash disabled=(not this.isEnabled)}}
122111
class="ai-spam__llm-selector"
123112
/>
124113
</div>
@@ -133,17 +122,17 @@ export default class AiSpam extends Component {
133122
</label>
134123
<textarea
135124
class="ai-spam__instructions-input"
125+
placeholder={{i18n
126+
"discourse_ai.spam.custom_instructions_placeholder"
127+
}}
136128
{{on
137129
"input"
138130
(fn (mut this.customInstructions) value="target.value")
139131
}}
140-
disabled={{not this.isEnabled}}
141132
>{{this.customInstructions}}</textarea>
142133
<DButton
143134
@action={{this.saveInstructions}}
144-
@icon="save"
145135
@label="save"
146-
@disabled={{not this.isEnabled}}
147136
class="ai-spam__instructions-save btn-primary"
148137
/>
149138
</div>

config/locales/client.en.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,9 @@ en:
140140
reports: "Reports"
141141
view_false_positives: "View false positives"
142142
view_missed_spam: "View missed spam"
143+
custom_instructions_placeholder: "Site-specific instructions for the AI to help identify spam more accurately."
144+
enable: "Enable"
145+
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."
143146

144147
usage:
145148
short_title: "Usage"

config/settings.yml

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ discourse_ai:
2525
ai_sentiment_backfill_post_max_age_days:
2626
default: 60
2727
hidden: true
28-
28+
2929

3030
ai_openai_dall_e_3_url: "https://api.openai.com/v1/images/generations"
3131
ai_openai_embeddings_url: "https://api.openai.com/v1/embeddings"
@@ -321,3 +321,25 @@ discourse_ai:
321321
type: list
322322
list_type: compact
323323
default: ""
324+
325+
ai_spam_detection_enabled:
326+
default: false
327+
#validator: "DiscourseAi::Configuration::LlmDependencyValidator"
328+
hidden: true
329+
330+
ai_spam_detection_model:
331+
default: ""
332+
type: enum
333+
allow_any: false
334+
enum: "DiscourseAi::Configuration::LlmEnumerator"
335+
#validator: "DiscourseAi::Configuration::LlmValidator"
336+
hidden: true
337+
338+
ai_spam_detection_custom_instructions:
339+
default: ""
340+
hidden: true
341+
342+
ai_spam_detection_model_allowed_seeded_models:
343+
default: ""
344+
hidden: true
345+
type: list
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# frozen_string_literal: true
2+
3+
require "rails_helper"
4+
5+
RSpec.describe DiscourseAi::Admin::AiSpamController do
6+
fab!(:admin) { Fabricate(:admin) }
7+
fab!(:user) { Fabricate(:user) }
8+
9+
fab!(:llm_model)
10+
11+
describe "#show" do
12+
context "when logged in as admin" do
13+
before do
14+
sign_in(admin)
15+
end
16+
17+
it "returns the serialized spam settings" do
18+
SiteSetting.ai_spam_detection_enabled = true
19+
SiteSetting.ai_spam_detection_custom_instructions = "Be strict with spam"
20+
SiteSetting.ai_spam_detection_model = llm_model.identifier
21+
22+
get "/admin/plugins/discourse-ai/ai-spam.json"
23+
24+
expect(response.status).to eq(200)
25+
26+
json = response.parsed_body
27+
expect(json["is_enabled"]).to eq(true)
28+
expect(json["selected_llm"]).to eq(llm_model.identifier)
29+
expect(json["custom_instructions"]).to eq("Be strict with spam")
30+
expect(json["available_llms"]).to be_an(Array)
31+
expect(json["stats"]).to be_present
32+
end
33+
34+
it "includes the correct stats structure" do
35+
get "/admin/plugins/discourse-ai/ai-spam.json"
36+
37+
json = response.parsed_body
38+
expect(json["stats"]).to include(
39+
"scanned_count",
40+
"spam_detected",
41+
"false_positives",
42+
"false_negatives"
43+
)
44+
end
45+
end
46+
47+
context "when not logged in as admin" do
48+
it "returns 404 for anonymous users" do
49+
get "/admin/plugins/discourse-ai/ai-spam.json"
50+
expect(response.status).to eq(404)
51+
end
52+
53+
it "returns 404 for regular users" do
54+
sign_in(user)
55+
get "/admin/plugins/discourse-ai/ai-spam.json"
56+
expect(response.status).to eq(404)
57+
end
58+
end
59+
60+
context "when plugin is disabled" do
61+
before do
62+
sign_in(admin)
63+
SiteSetting.discourse_ai_enabled = false
64+
end
65+
66+
it "returns 404" do
67+
get "/admin/plugins/discourse-ai/ai-spam.json"
68+
expect(response.status).to eq(404)
69+
end
70+
end
71+
end
72+
end

0 commit comments

Comments
 (0)