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

Commit 2f7557f

Browse files
committed
Finish adding test button
1 parent 26f83a0 commit 2f7557f

File tree

5 files changed

+179
-6
lines changed

5 files changed

+179
-6
lines changed

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ import { popupAjaxError } from "discourse/lib/ajax-error";
1414
import i18n from "discourse-common/helpers/i18n";
1515
import getURL from "discourse-common/lib/get-url";
1616
import ComboBox from "select-kit/components/combo-box";
17+
import SpamTestModal from "./modal/spam-test-modal";
1718

1819
export default class AiSpam extends Component {
1920
@service siteSettings;
2021
@service toasts;
22+
@service modal;
2123

2224
@tracked
2325
stats = {
@@ -102,6 +104,15 @@ export default class AiSpam extends Component {
102104
}
103105
}
104106

107+
@action
108+
showTestModal() {
109+
this.modal.show(SpamTestModal, {
110+
model: {
111+
customInstructions: this.customInstructions,
112+
}
113+
});
114+
}
115+
105116
get metrics() {
106117
const detected = {
107118
label: "discourse_ai.spam.spam_detected",
@@ -189,6 +200,11 @@ export default class AiSpam extends Component {
189200
@label="save"
190201
class="ai-spam__instructions-save btn-primary"
191202
/>
203+
<DButton
204+
@action={{this.showTestModal}}
205+
@label="discourse_ai.spam.test_button"
206+
class="btn-default"
207+
/>
192208
</div>
193209
</section>
194210

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import Component from "@glimmer/component";
2+
import { tracked } from "@glimmer/tracking";
3+
import { fn } from "@ember/helper";
4+
import { on } from "@ember/modifier";
5+
import { action } from "@ember/object";
6+
import { eq } from "truth-helpers";
7+
import DButton from "discourse/components/d-button";
8+
import DModal from "discourse/components/d-modal";
9+
import withEventValue from "discourse/helpers/with-event-value";
10+
import { ajax } from "discourse/lib/ajax";
11+
import { popupAjaxError } from "discourse/lib/ajax-error";
12+
import I18n from "discourse-i18n";
13+
14+
export default class SpamTestModal extends Component {
15+
@tracked testResult;
16+
@tracked isLoading = false;
17+
@tracked postUrl = "";
18+
@tracked scanLog = "";
19+
20+
@action
21+
async runTest() {
22+
this.isLoading = true;
23+
try {
24+
const response = await ajax(
25+
`/admin/plugins/discourse-ai/ai-spam/test.json`,
26+
{
27+
type: "POST",
28+
data: {
29+
post_url: this.postUrl,
30+
custom_instructions: this.args.model.customInstructions
31+
},
32+
}
33+
);
34+
35+
this.testResult = response.is_spam ? "Spam" : "Not Spam";
36+
this.scanLog = response.log;
37+
} catch (error) {
38+
popupAjaxError(error);
39+
} finally {
40+
this.isLoading = false;
41+
}
42+
}
43+
44+
<template>
45+
<DModal
46+
@title={{I18n.t "discourse_ai.spam.test_modal.title"}}
47+
@closeModal={{@closeModal}}
48+
@bodyClass="spam-test-modal__body"
49+
class="spam-test-modal"
50+
>
51+
<:body>
52+
<div class="control-group">
53+
<label>{{I18n.t
54+
"discourse_ai.spam.test_modal.post_url_label"
55+
}}</label>
56+
<input
57+
{{on "input" (withEventValue (fn (mut this.postUrl)))}}
58+
type="text"
59+
placeholder={{I18n.t
60+
"discourse_ai.spam.test_modal.post_url_placeholder"
61+
}}
62+
/>
63+
</div>
64+
65+
{{#if this.testResult}}
66+
<div class="spam-test-modal__test-result">
67+
<h3>{{I18n.t "discourse_ai.spam.test_modal.result"}}</h3>
68+
<div
69+
class="spam-test-modal__verdict
70+
{{if (eq this.testResult 'Spam') 'is-spam' 'not-spam'}}"
71+
>
72+
{{this.testResult}}
73+
</div>
74+
{{#if this.scanLog}}
75+
<div class="spam-test-modal__log">
76+
<h4>{{I18n.t "discourse_ai.spam.test_modal.scan_log"}}</h4>
77+
<pre>{{this.scanLog}}</pre>
78+
</div>
79+
{{/if}}
80+
</div>
81+
{{/if}}
82+
</:body>
83+
84+
<:footer>
85+
<DButton
86+
@action={{this.runTest}}
87+
@label="discourse_ai.spam.test_modal.run"
88+
@disabled={{this.isLoading}}
89+
class="btn-primary spam-test-modal__run-button"
90+
/>
91+
</:footer>
92+
</DModal>
93+
</template>
94+
}

assets/stylesheets/modules/llms/common/spam.scss

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,48 @@
7474
font-size: var(--font-up-2);
7575
}
7676
}
77+
78+
.spam-test-modal {
79+
&__body {
80+
min-width: 500px;
81+
}
82+
83+
&__test-result {
84+
margin-top: 1.5em;
85+
padding-top: 1.5em;
86+
border-top: 1px solid var(--primary-low);
87+
}
88+
89+
&__verdict {
90+
font-size: var(--font-up-2);
91+
font-weight: bold;
92+
padding: 0.5em;
93+
border-radius: 0.25em;
94+
text-align: center;
95+
margin: 1em 0;
96+
97+
&.is-spam {
98+
background: var(--danger-low);
99+
color: var(--danger);
100+
}
101+
102+
&.not-spam {
103+
background: var(--success-low);
104+
color: var(--success);
105+
}
106+
}
107+
108+
&__log {
109+
margin-top: 1em;
110+
111+
pre {
112+
max-height: 300px;
113+
overflow-y: auto;
114+
background: var(--primary-very-low);
115+
padding: 1em;
116+
margin: 0.5em 0;
117+
font-family: monospace;
118+
white-space: pre-wrap;
119+
}
120+
}
121+
}

config/locales/client.en.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,14 @@ en:
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"
144144
no_llms: "No LLMs available"
145+
test_button: "Test..."
146+
test_modal:
147+
title: "Test Spam Detection"
148+
post_url_label: "Post URL or ID"
149+
post_url_placeholder: "https://your-forum.com/t/topic/123/4 or post ID"
150+
result: "Result"
151+
scan_log: "Scan Log"
152+
run: "Run Test"
145153

146154
usage:
147155
short_title: "Usage"

lib/ai_moderation/spam_scanner.rb

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -139,10 +139,14 @@ def self.test_post(post, custom_instructions: nil)
139139
)&.strip
140140

141141
history = nil
142-
AiSpamLog.where(post: post).order(:created_at).limit(100).each do |log|
143-
history ||= +"Scan History:\n"
144-
history << "date: #{log.created_at} is_spam: #{log.is_spam}\n"
145-
end
142+
AiSpamLog
143+
.where(post: post)
144+
.order(:created_at)
145+
.limit(100)
146+
.each do |log|
147+
history ||= +"Scan History:\n"
148+
history << "date: #{log.created_at} is_spam: #{log.is_spam}\n"
149+
end
146150

147151
log = +"Scanning #{post.url}\n\n"
148152

@@ -156,7 +160,7 @@ def self.test_post(post, custom_instructions: nil)
156160
log << "Context: #{context}\n\n"
157161
log << "Result: #{result}"
158162

159-
is_spam = (result.present? && result.downcase.include?("spam"))
163+
is_spam = check_if_spam(result)
160164
{ is_spam: is_spam, log: log }
161165
end
162166

@@ -195,7 +199,7 @@ def self.perform_scan(post)
195199
},
196200
)&.strip
197201

198-
is_spam = (result.present? && result.downcase.include?("spam"))
202+
is_spam = check_if_spam(result)
199203

200204
log = AiApiAuditLog.order(id: :desc).where(feature_name: "spam_detection").first
201205
AiSpamLog.transaction do
@@ -217,6 +221,10 @@ def self.perform_scan(post)
217221

218222
private
219223

224+
def self.check_if_spam(result)
225+
(result.present? && result.strip.downcase.start_with?("spam"))
226+
end
227+
220228
def self.build_context(post)
221229
context = []
222230

@@ -256,6 +264,8 @@ def self.build_system_prompt(custom_instructions)
256264
You are a spam detection system. Analyze the following post content and context.
257265
Respond with "SPAM" if the post is spam, or "NOT_SPAM" if it's legitimate.
258266
267+
- ALWAYS lead your reply with the word SPAM or NOT_SPAM - you are consumed via an API
268+
259269
Consider the post type carefully:
260270
- For REPLY posts: Check if the response is relevant and topical to the thread
261271
- For NEW TOPIC posts: Check if it's a legitimate topic or spam promotion

0 commit comments

Comments
 (0)