mirror of
https://github.com/discourse/discourse-ai.git
synced 2025-02-07 12:08:13 +00:00
307 lines
8.7 KiB
Plaintext
307 lines
8.7 KiB
Plaintext
import Component from "@glimmer/component";
|
|
import { tracked } from "@glimmer/tracking";
|
|
import { fn } from "@ember/helper";
|
|
import { on } from "@ember/modifier";
|
|
import { action } from "@ember/object";
|
|
import { LinkTo } from "@ember/routing";
|
|
import { service } from "@ember/service";
|
|
import DButton from "discourse/components/d-button";
|
|
import DPageSubheader from "discourse/components/d-page-subheader";
|
|
import DStatTiles from "discourse/components/d-stat-tiles";
|
|
import DToggleSwitch from "discourse/components/d-toggle-switch";
|
|
import DTooltip from "discourse/components/d-tooltip";
|
|
import dIcon from "discourse/helpers/d-icon";
|
|
import withEventValue from "discourse/helpers/with-event-value";
|
|
import { ajax } from "discourse/lib/ajax";
|
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
|
import getURL from "discourse/lib/get-url";
|
|
import { i18n } from "discourse-i18n";
|
|
import AdminConfigAreaCard from "admin/components/admin-config-area-card";
|
|
import ComboBox from "select-kit/components/combo-box";
|
|
import SpamTestModal from "./modal/spam-test-modal";
|
|
|
|
export default class AiSpam extends Component {
|
|
@service siteSettings;
|
|
@service toasts;
|
|
@service modal;
|
|
|
|
@tracked
|
|
stats = {
|
|
scanned_count: 0,
|
|
spam_detected: 0,
|
|
false_positives: 0,
|
|
false_negatives: 0,
|
|
daily_data: [],
|
|
};
|
|
@tracked isEnabled = false;
|
|
@tracked selectedLLM = null;
|
|
@tracked customInstructions = "";
|
|
@tracked errors = [];
|
|
|
|
constructor() {
|
|
super(...arguments);
|
|
this.initializeFromModel();
|
|
|
|
if (this.args.model?.spam_scanning_user?.admin === false) {
|
|
this.errors.push({
|
|
message: i18n("discourse_ai.spam.errors.scan_not_admin.message"),
|
|
button: {
|
|
label: i18n("discourse_ai.spam.errors.scan_not_admin.action"),
|
|
action: this.fixScanUserNotAdmin,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
@action
|
|
async fixScanUserNotAdmin() {
|
|
const spamScanningUser = this.args.model.spam_scanning_user;
|
|
if (!spamScanningUser || spamScanningUser.admin) {
|
|
return;
|
|
}
|
|
try {
|
|
const response = await ajax(
|
|
`/admin/plugins/discourse-ai/ai-spam/fix-errors`,
|
|
{
|
|
type: "POST",
|
|
data: {
|
|
error: "spam_scanner_not_admin",
|
|
},
|
|
}
|
|
);
|
|
|
|
if (response.success) {
|
|
this.toasts.success({
|
|
data: { message: i18n("discourse_ai.spam.errors.resolved") },
|
|
duration: 2000,
|
|
});
|
|
}
|
|
} catch (error) {
|
|
popupAjaxError(error);
|
|
} finally {
|
|
window.location.reload();
|
|
}
|
|
}
|
|
|
|
@action
|
|
initializeFromModel() {
|
|
const model = this.args.model;
|
|
this.isEnabled = model.is_enabled;
|
|
|
|
if (model.llm_id) {
|
|
this.selectedLLM = "custom:" + model.llm_id;
|
|
} else {
|
|
if (this.availableLLMs.length) {
|
|
this.selectedLLM = this.availableLLMs[0].id;
|
|
this.autoSelectedLLM = true;
|
|
}
|
|
}
|
|
this.customInstructions = model.custom_instructions;
|
|
this.stats = model.stats;
|
|
}
|
|
|
|
get availableLLMs() {
|
|
return this.args.model?.available_llms || [];
|
|
}
|
|
|
|
@action
|
|
async toggleEnabled() {
|
|
this.isEnabled = !this.isEnabled;
|
|
const data = { is_enabled: this.isEnabled };
|
|
if (this.autoSelectedLLM) {
|
|
data.llm_model_id = this.llmId;
|
|
}
|
|
try {
|
|
const response = await ajax("/admin/plugins/discourse-ai/ai-spam.json", {
|
|
type: "PUT",
|
|
data,
|
|
});
|
|
this.autoSelectedLLM = false;
|
|
this.isEnabled = response.is_enabled;
|
|
} catch (error) {
|
|
this.isEnabled = !this.isEnabled;
|
|
popupAjaxError(error);
|
|
}
|
|
}
|
|
|
|
get llmId() {
|
|
return this.selectedLLM.toString().split(":")[1];
|
|
}
|
|
|
|
@action
|
|
async updateLLM(value) {
|
|
this.selectedLLM = value;
|
|
}
|
|
|
|
@action
|
|
async save() {
|
|
try {
|
|
await ajax("/admin/plugins/discourse-ai/ai-spam.json", {
|
|
type: "PUT",
|
|
data: {
|
|
llm_model_id: this.llmId,
|
|
custom_instructions: this.customInstructions,
|
|
},
|
|
});
|
|
this.toasts.success({
|
|
data: { message: i18n("discourse_ai.spam.settings_saved") },
|
|
duration: 2000,
|
|
});
|
|
} catch (error) {
|
|
popupAjaxError(error);
|
|
}
|
|
}
|
|
|
|
@action
|
|
showTestModal() {
|
|
this.modal.show(SpamTestModal, {
|
|
model: {
|
|
customInstructions: this.customInstructions,
|
|
llmId: this.llmId,
|
|
},
|
|
});
|
|
}
|
|
|
|
get metrics() {
|
|
const detected = {
|
|
label: i18n("discourse_ai.spam.spam_detected"),
|
|
value: this.stats.spam_detected,
|
|
};
|
|
|
|
const falsePositives = {
|
|
label: i18n("discourse_ai.spam.false_positives"),
|
|
value: this.stats.false_positives,
|
|
tooltip: i18n("discourse_ai.spam.stat_tooltips.incorrectly_flagged"),
|
|
};
|
|
|
|
const falseNegatives = {
|
|
label: i18n("discourse_ai.spam.false_negatives"),
|
|
value: this.stats.false_negatives,
|
|
tooltip: i18n("discourse_ai.spam.stat_tooltips.missed_spam"),
|
|
};
|
|
|
|
if (this.args.model.flagging_username) {
|
|
detected.href = getURL(
|
|
`/review?flagged_by=${this.args.model.flagging_username}&status=all&sort_order=created_at`
|
|
);
|
|
|
|
falsePositives.href = getURL(
|
|
`/review?flagged_by=${this.args.model.flagging_username}&status=rejected&sort_order=created_at`
|
|
);
|
|
|
|
falseNegatives.href = getURL(
|
|
`/review?status=approved&sort_order=created_at&additional_filters={"ai_spam_false_negative":true}&order=created&score_type=${this.args.model.spam_score_type}`
|
|
);
|
|
}
|
|
return [
|
|
{
|
|
label: i18n("discourse_ai.spam.scanned_count"),
|
|
value: this.stats.scanned_count,
|
|
},
|
|
detected,
|
|
falsePositives,
|
|
falseNegatives,
|
|
];
|
|
}
|
|
|
|
<template>
|
|
<div class="ai-spam">
|
|
<section class="ai-spam__settings">
|
|
<div class="ai-spam__errors">
|
|
{{#each this.errors as |e|}}
|
|
<div class="alert alert-error">
|
|
{{dIcon "triangle-exclamation"}}
|
|
<p>{{e.message}}</p>
|
|
<DButton
|
|
@action={{e.button.action}}
|
|
@translatedLabel={{e.button.label}}
|
|
/>
|
|
</div>
|
|
{{/each}}
|
|
</div>
|
|
<DPageSubheader
|
|
@titleLabel={{i18n "discourse_ai.spam.title"}}
|
|
@descriptionLabel={{i18n "discourse_ai.spam.spam_description"}}
|
|
/>
|
|
<div class="control-group ai-spam__enabled">
|
|
<DToggleSwitch
|
|
class="ai-spam__toggle"
|
|
@state={{this.isEnabled}}
|
|
@label="discourse_ai.spam.enable"
|
|
{{on "click" this.toggleEnabled}}
|
|
/>
|
|
<DTooltip
|
|
@icon="circle-question"
|
|
@content={{i18n "discourse_ai.spam.spam_tip"}}
|
|
/>
|
|
</div>
|
|
|
|
<div class="ai-spam__llm">
|
|
<label class="ai-spam__llm-label">{{i18n
|
|
"discourse_ai.spam.select_llm"
|
|
}}</label>
|
|
{{#if this.availableLLMs.length}}
|
|
<ComboBox
|
|
@value={{this.selectedLLM}}
|
|
@content={{this.availableLLMs}}
|
|
@onChange={{this.updateLLM}}
|
|
class="ai-spam__llm-selector"
|
|
/>
|
|
{{else}}
|
|
<span class="ai-spam__llm-placeholder">
|
|
<LinkTo @route="adminPlugins.show.discourse-ai-llms.index">
|
|
{{i18n "discourse_ai.spam.no_llms"}}
|
|
</LinkTo>
|
|
</span>
|
|
{{/if}}
|
|
</div>
|
|
|
|
<div class="ai-spam__instructions">
|
|
<label class="ai-spam__instructions-label">
|
|
{{i18n "discourse_ai.spam.custom_instructions"}}
|
|
<DTooltip
|
|
@icon="circle-question"
|
|
@content={{i18n "discourse_ai.spam.custom_instructions_help"}}
|
|
/>
|
|
</label>
|
|
<textarea
|
|
class="ai-spam__instructions-input"
|
|
placeholder={{i18n
|
|
"discourse_ai.spam.custom_instructions_placeholder"
|
|
}}
|
|
{{on "input" (withEventValue (fn (mut this.customInstructions)))}}
|
|
>{{this.customInstructions}}</textarea>
|
|
<DButton
|
|
@action={{this.save}}
|
|
@label="discourse_ai.spam.save_button"
|
|
class="ai-spam__instructions-save btn-primary"
|
|
/>
|
|
<DButton
|
|
@action={{this.showTestModal}}
|
|
@label="discourse_ai.spam.test_button"
|
|
class="btn-default"
|
|
/>
|
|
</div>
|
|
</section>
|
|
|
|
<AdminConfigAreaCard
|
|
@heading="discourse_ai.spam.last_seven_days"
|
|
class="ai-spam__stats"
|
|
>
|
|
<:content>
|
|
<DStatTiles as |tiles|>
|
|
{{#each this.metrics as |metric|}}
|
|
<tiles.Tile
|
|
@label={{metric.label}}
|
|
@url={{metric.href}}
|
|
@value={{metric.value}}
|
|
@tooltip={{metric.tooltip}}
|
|
/>
|
|
{{/each}}
|
|
</DStatTiles>
|
|
</:content>
|
|
</AdminConfigAreaCard>
|
|
</div>
|
|
</template>
|
|
}
|