Skip to content

FEATURE: Configure persona backed features in admin panel #1245

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Apr 10, 2025
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