Skip to content
This repository was archived by the owner on Jul 22, 2025. It is now read-only.
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { ajax } from "discourse/lib/ajax";
import DiscourseRoute from "discourse/routes/discourse";
import SiteSetting from "admin/models/site-setting";

export default class AdminPluginsShowDiscourseAiFeaturesEdit extends DiscourseRoute {
async model(params) {
const allFeatures = this.modelFor(
"adminPlugins.show.discourse-ai-features"
);
const id = parseInt(params.id, 10);
const currentFeature = allFeatures.find((feature) => feature.id === id);

const { site_settings } = await ajax("/admin/config/site_settings.json", {
data: {
filter_area: `ai-features/${currentFeature.ref}`,
plugin: "discourse-ai",
category: "discourse_ai",
},
});

currentFeature.feature_settings = site_settings.map((setting) =>
SiteSetting.create(setting)
);

return currentFeature;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { service } from "@ember/service";
import DiscourseRoute from "discourse/routes/discourse";

export default class AdminPluginsShowDiscourseAiFeatures extends DiscourseRoute {
@service store;

async model() {
return this.store.findAll("ai-feature");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import RouteTemplate from "ember-route-template";
import BackButton from "discourse/components/back-button";
import SiteSettingComponent from "admin/components/site-setting";

export default RouteTemplate(
<template>
<BackButton
@route="adminPlugins.show.discourse-ai-features"
@label="discourse_ai.features.back"
/>
<section class="ai-feature-editor__header">
<h2>{{@model.name}}</h2>
<p>{{@model.description}}</p>
</section>

<section class="ai-feature-editor">
{{#each @model.feature_settings as |setting|}}
<div>
<SiteSettingComponent @setting={{setting}} />
</div>
{{/each}}
</section>
</template>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import Component from "@glimmer/component";
import { service } from "@ember/service";
import RouteTemplate from "ember-route-template";
import { gt } from "truth-helpers";
import DBreadcrumbsItem from "discourse/components/d-breadcrumbs-item";
import DButton from "discourse/components/d-button";
import DPageSubheader from "discourse/components/d-page-subheader";
import { i18n } from "discourse-i18n";

export default RouteTemplate(
class extends Component {
@service adminPluginNavManager;

get tableHeaders() {
const prefix = "discourse_ai.features.list.header";
return [
i18n(`${prefix}.name`),
i18n(`${prefix}.persona`),
i18n(`${prefix}.groups`),
"",
];
}

get configuredFeatures() {
return this.args.model.filter(
(feature) => feature.enable_setting.value === true
);
}

get unconfiguredFeatures() {
return this.args.model.filter(
(feature) => feature.enable_setting.value === false
);
}

<template>
<DBreadcrumbsItem
@path="/admin/plugins/{{this.adminPluginNavManager.currentPlugin.name}}/ai-features"
@label={{i18n "discourse_ai.features.short_title"}}
/>
<section class="ai-feature-list admin-detail">
<DPageSubheader
@titleLabel={{i18n "discourse_ai.features.short_title"}}
@descriptionLabel={{i18n "discourse_ai.features.description"}}
@learnMoreUrl="todo"
/>

{{#if (gt this.configuredFeatures.length 0)}}
<div class="ai-feature-list__configured-features">
<h3>{{i18n "discourse_ai.features.list.configured_features"}}</h3>

<table class="d-admin-table">
<thead>
<tr>
{{#each this.tableHeaders as |header|}}
<th>{{header}}</th>
{{/each}}
</tr>
</thead>

<tbody>
{{#each this.configuredFeatures as |feature|}}
<tr
class="ai-feature-list__row d-admin-row__content"
data-feature-name={{feature.name}}
>
<td class="d-admin-row__overview ai-feature-list__row-item">
<span class="ai-feature-list__row-item-name">
<strong>
{{feature.name}}
</strong>
</span>
<span class="ai-feature-list__row-item-description">
{{feature.description}}
</span>
</td>
<td
class="d-admin-row__detail ai-feature-list__row-item ai-feature-list__persona"
>
<DButton
class="btn-flat btn-small ai-feature-list__row-item-persona"
@translatedLabel={{feature.persona.name}}
@route="adminPlugins.show.discourse-ai-personas.edit"
@routeModels={{feature.persona.id}}
/>
</td>
<td
class="d-admin-row__detail ai-feature-list__row-item ai-feature-list__groups"
>
{{#if (gt feature.persona.allowed_groups.length 0)}}
<ul class="ai-feature-list__row-item-groups">
{{#each feature.persona.allowed_groups as |group|}}
<li>{{group.name}}</li>
{{/each}}
</ul>
{{/if}}
</td>
<td class="d-admin-row_controls">
<DButton
class="btn-small edit"
@label="discourse_ai.features.list.edit"
@route="adminPlugins.show.discourse-ai-features.edit"
@routeModels={{feature.id}}
/>
</td>
</tr>
{{/each}}
</tbody>
</table>
</div>
{{/if}}

{{#if (gt this.unconfiguredFeatures.length 0)}}
<div class="ai-feature-list__unconfigured-features">
<h3>{{i18n "discourse_ai.features.list.unconfigured_features"}}</h3>

<table class="d-admin-table">
<thead>
<tr>
<th>{{i18n "discourse_ai.features.list.header.name"}}</th>
<th></th>
</tr>
</thead>

<tbody>
{{#each this.unconfiguredFeatures as |feature|}}
<tr class="ai-feature-list__row d-admin-row__content">
<td class="d-admin-row__overview ai-feature-list__row-item">
<span class="ai-feature-list__row-item-name">
<strong>
{{feature.name}}
</strong>
</span>
<span class="ai-feature-list__row-item-description">
{{feature.description}}
</span>
</td>

<td class="d-admin-row_controls">
<DButton
class="btn-small"
@label="discourse_ai.features.list.set_up"
@route="adminPlugins.show.discourse-ai-features.edit"
@routeModels={{feature.id}}
/>
</td>
</tr>
{{/each}}
</tbody>
</table>
</div>
{{/if}}
</section>
</template>
}
);
36 changes: 36 additions & 0 deletions app/controllers/discourse_ai/admin/ai_features_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# frozen_string_literal: true

module DiscourseAi
module Admin
class AiFeaturesController < ::Admin::AdminController
requires_plugin ::DiscourseAi::PLUGIN_NAME

def index
render json: serialize_features(DiscourseAi::Features.features)
end

def edit
raise Discourse::InvalidParameters.new(:id) if params[:id].blank?
render json: serialize_feature(DiscourseAi::Features.find_feature_by_id(params[:id].to_i))
end

private

def serialize_features(features)
features.map { |feature| feature.merge(persona: serialize_persona(feature[:persona])) }
end

def serialize_feature(feature)
return nil if feature.blank?

feature.merge(persona: serialize_persona(feature[:persona]))
end

def serialize_persona(persona)
return nil if persona.blank?

serialize_data(persona, AiFeaturesPersonaSerializer, root: false)
end
end
end
end
2 changes: 2 additions & 0 deletions app/jobs/regular/stream_discord_reply.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ class StreamDiscordReply < ::Jobs::Base
def execute(args)
interaction = args[:interaction]

return unless SiteSetting.ai_discord_search_enabled

if SiteSetting.ai_discord_search_mode == "persona"
DiscourseAi::Discord::Bot::PersonaReplier.new(interaction).handle_interaction!
else
Expand Down
12 changes: 12 additions & 0 deletions app/serializers/ai_features_persona_serializer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# frozen_string_literal: true

class AiFeaturesPersonaSerializer < ApplicationSerializer
attributes :id, :name, :system_prompt, :allowed_groups, :enabled

def allowed_groups
Group
.where(id: object.allowed_group_ids)
.pluck(:id, :name)
.map { |id, name| { id: id, name: name } }
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,9 @@ export default {
this.route("edit", { path: "/:id/edit" });
}
);

this.route("discourse-ai-features", { path: "ai-features" }, function () {
this.route("edit", { path: "/:id/edit" });
});
},
};
21 changes: 21 additions & 0 deletions assets/javascripts/discourse/admin/adapters/ai-feature.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import RestAdapter from "discourse/adapters/rest";

export default class AiFeatureAdapter extends RestAdapter {
jsonMode = true;

basePath() {
return "/admin/plugins/discourse-ai/";
}

pathFor(store, type, findArgs) {
// removes underscores which are implemented in base
let path =
this.basePath(store, type, findArgs) +
store.pluralize(this.apiNameFor(type));
return this.appendQueryParams(path, findArgs);
}

apiNameFor() {
return "ai-feature";
}
}
15 changes: 15 additions & 0 deletions assets/javascripts/discourse/admin/models/ai-feature.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import RestModel from "discourse/models/rest";

export default class AiFeature extends RestModel {
createProperties() {
return this.getProperties(
"id",
"name",
"ref",
"description",
"enable_setting",
"persona",
"persona_setting"
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ export default {
route: "adminPlugins.show.discourse-ai-spam",
description: "discourse_ai.spam.spam_description",
},
// TODO(@keegan / @roman): Uncomment this when structured output is merged
// {
// label: "discourse_ai.features.short_title",
// route: "adminPlugins.show.discourse-ai-features",
// description: "discourse_ai.features.description",
// },
]);
});
},
Expand Down
Loading