Skip to content

Commit 4de39a0

Browse files
authored
FEATURE: Configure persona backed features in admin panel (#1245)
In this feature update, we add the UI for the ability to easily configure persona backed AI-features. The feature will still be hidden until structured responses are complete.
1 parent 129ced9 commit 4de39a0

File tree

24 files changed

+713
-12
lines changed

24 files changed

+713
-12
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { ajax } from "discourse/lib/ajax";
2+
import DiscourseRoute from "discourse/routes/discourse";
3+
import SiteSetting from "admin/models/site-setting";
4+
5+
export default class AdminPluginsShowDiscourseAiFeaturesEdit extends DiscourseRoute {
6+
async model(params) {
7+
const allFeatures = this.modelFor(
8+
"adminPlugins.show.discourse-ai-features"
9+
);
10+
const id = parseInt(params.id, 10);
11+
const currentFeature = allFeatures.find((feature) => feature.id === id);
12+
13+
const { site_settings } = await ajax("/admin/config/site_settings.json", {
14+
data: {
15+
filter_area: `ai-features/${currentFeature.ref}`,
16+
plugin: "discourse-ai",
17+
category: "discourse_ai",
18+
},
19+
});
20+
21+
currentFeature.feature_settings = site_settings.map((setting) =>
22+
SiteSetting.create(setting)
23+
);
24+
25+
return currentFeature;
26+
}
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { service } from "@ember/service";
2+
import DiscourseRoute from "discourse/routes/discourse";
3+
4+
export default class AdminPluginsShowDiscourseAiFeatures extends DiscourseRoute {
5+
@service store;
6+
7+
async model() {
8+
return this.store.findAll("ai-feature");
9+
}
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import RouteTemplate from "ember-route-template";
2+
import BackButton from "discourse/components/back-button";
3+
import SiteSettingComponent from "admin/components/site-setting";
4+
5+
export default RouteTemplate(
6+
<template>
7+
<BackButton
8+
@route="adminPlugins.show.discourse-ai-features"
9+
@label="discourse_ai.features.back"
10+
/>
11+
<section class="ai-feature-editor__header">
12+
<h2>{{@model.name}}</h2>
13+
<p>{{@model.description}}</p>
14+
</section>
15+
16+
<section class="ai-feature-editor">
17+
{{#each @model.feature_settings as |setting|}}
18+
<div>
19+
<SiteSettingComponent @setting={{setting}} />
20+
</div>
21+
{{/each}}
22+
</section>
23+
</template>
24+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import Component from "@glimmer/component";
2+
import { service } from "@ember/service";
3+
import RouteTemplate from "ember-route-template";
4+
import { gt } from "truth-helpers";
5+
import DBreadcrumbsItem from "discourse/components/d-breadcrumbs-item";
6+
import DButton from "discourse/components/d-button";
7+
import DPageSubheader from "discourse/components/d-page-subheader";
8+
import { i18n } from "discourse-i18n";
9+
10+
export default RouteTemplate(
11+
class extends Component {
12+
@service adminPluginNavManager;
13+
14+
get tableHeaders() {
15+
const prefix = "discourse_ai.features.list.header";
16+
return [
17+
i18n(`${prefix}.name`),
18+
i18n(`${prefix}.persona`),
19+
i18n(`${prefix}.groups`),
20+
"",
21+
];
22+
}
23+
24+
get configuredFeatures() {
25+
return this.args.model.filter(
26+
(feature) => feature.enable_setting.value === true
27+
);
28+
}
29+
30+
get unconfiguredFeatures() {
31+
return this.args.model.filter(
32+
(feature) => feature.enable_setting.value === false
33+
);
34+
}
35+
36+
<template>
37+
<DBreadcrumbsItem
38+
@path="/admin/plugins/{{this.adminPluginNavManager.currentPlugin.name}}/ai-features"
39+
@label={{i18n "discourse_ai.features.short_title"}}
40+
/>
41+
<section class="ai-feature-list admin-detail">
42+
<DPageSubheader
43+
@titleLabel={{i18n "discourse_ai.features.short_title"}}
44+
@descriptionLabel={{i18n "discourse_ai.features.description"}}
45+
@learnMoreUrl="todo"
46+
/>
47+
48+
{{#if (gt this.configuredFeatures.length 0)}}
49+
<div class="ai-feature-list__configured-features">
50+
<h3>{{i18n "discourse_ai.features.list.configured_features"}}</h3>
51+
52+
<table class="d-admin-table">
53+
<thead>
54+
<tr>
55+
{{#each this.tableHeaders as |header|}}
56+
<th>{{header}}</th>
57+
{{/each}}
58+
</tr>
59+
</thead>
60+
61+
<tbody>
62+
{{#each this.configuredFeatures as |feature|}}
63+
<tr
64+
class="ai-feature-list__row d-admin-row__content"
65+
data-feature-name={{feature.name}}
66+
>
67+
<td class="d-admin-row__overview ai-feature-list__row-item">
68+
<span class="ai-feature-list__row-item-name">
69+
<strong>
70+
{{feature.name}}
71+
</strong>
72+
</span>
73+
<span class="ai-feature-list__row-item-description">
74+
{{feature.description}}
75+
</span>
76+
</td>
77+
<td
78+
class="d-admin-row__detail ai-feature-list__row-item ai-feature-list__persona"
79+
>
80+
<DButton
81+
class="btn-flat btn-small ai-feature-list__row-item-persona"
82+
@translatedLabel={{feature.persona.name}}
83+
@route="adminPlugins.show.discourse-ai-personas.edit"
84+
@routeModels={{feature.persona.id}}
85+
/>
86+
</td>
87+
<td
88+
class="d-admin-row__detail ai-feature-list__row-item ai-feature-list__groups"
89+
>
90+
{{#if (gt feature.persona.allowed_groups.length 0)}}
91+
<ul class="ai-feature-list__row-item-groups">
92+
{{#each feature.persona.allowed_groups as |group|}}
93+
<li>{{group.name}}</li>
94+
{{/each}}
95+
</ul>
96+
{{/if}}
97+
</td>
98+
<td class="d-admin-row_controls">
99+
<DButton
100+
class="btn-small edit"
101+
@label="discourse_ai.features.list.edit"
102+
@route="adminPlugins.show.discourse-ai-features.edit"
103+
@routeModels={{feature.id}}
104+
/>
105+
</td>
106+
</tr>
107+
{{/each}}
108+
</tbody>
109+
</table>
110+
</div>
111+
{{/if}}
112+
113+
{{#if (gt this.unconfiguredFeatures.length 0)}}
114+
<div class="ai-feature-list__unconfigured-features">
115+
<h3>{{i18n "discourse_ai.features.list.unconfigured_features"}}</h3>
116+
117+
<table class="d-admin-table">
118+
<thead>
119+
<tr>
120+
<th>{{i18n "discourse_ai.features.list.header.name"}}</th>
121+
<th></th>
122+
</tr>
123+
</thead>
124+
125+
<tbody>
126+
{{#each this.unconfiguredFeatures as |feature|}}
127+
<tr class="ai-feature-list__row d-admin-row__content">
128+
<td class="d-admin-row__overview ai-feature-list__row-item">
129+
<span class="ai-feature-list__row-item-name">
130+
<strong>
131+
{{feature.name}}
132+
</strong>
133+
</span>
134+
<span class="ai-feature-list__row-item-description">
135+
{{feature.description}}
136+
</span>
137+
</td>
138+
139+
<td class="d-admin-row_controls">
140+
<DButton
141+
class="btn-small"
142+
@label="discourse_ai.features.list.set_up"
143+
@route="adminPlugins.show.discourse-ai-features.edit"
144+
@routeModels={{feature.id}}
145+
/>
146+
</td>
147+
</tr>
148+
{{/each}}
149+
</tbody>
150+
</table>
151+
</div>
152+
{{/if}}
153+
</section>
154+
</template>
155+
}
156+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# frozen_string_literal: true
2+
3+
module DiscourseAi
4+
module Admin
5+
class AiFeaturesController < ::Admin::AdminController
6+
requires_plugin ::DiscourseAi::PLUGIN_NAME
7+
8+
def index
9+
render json: serialize_features(DiscourseAi::Features.features)
10+
end
11+
12+
def edit
13+
raise Discourse::InvalidParameters.new(:id) if params[:id].blank?
14+
render json: serialize_feature(DiscourseAi::Features.find_feature_by_id(params[:id].to_i))
15+
end
16+
17+
private
18+
19+
def serialize_features(features)
20+
features.map { |feature| feature.merge(persona: serialize_persona(feature[:persona])) }
21+
end
22+
23+
def serialize_feature(feature)
24+
return nil if feature.blank?
25+
26+
feature.merge(persona: serialize_persona(feature[:persona]))
27+
end
28+
29+
def serialize_persona(persona)
30+
return nil if persona.blank?
31+
32+
serialize_data(persona, AiFeaturesPersonaSerializer, root: false)
33+
end
34+
end
35+
end
36+
end

app/jobs/regular/stream_discord_reply.rb

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ class StreamDiscordReply < ::Jobs::Base
77
def execute(args)
88
interaction = args[:interaction]
99

10+
return unless SiteSetting.ai_discord_search_enabled
11+
1012
if SiteSetting.ai_discord_search_mode == "persona"
1113
DiscourseAi::Discord::Bot::PersonaReplier.new(interaction).handle_interaction!
1214
else
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# frozen_string_literal: true
2+
3+
class AiFeaturesPersonaSerializer < ApplicationSerializer
4+
attributes :id, :name, :system_prompt, :allowed_groups, :enabled
5+
6+
def allowed_groups
7+
Group
8+
.where(id: object.allowed_group_ids)
9+
.pluck(:id, :name)
10+
.map { |id, name| { id: id, name: name } }
11+
end
12+
end

assets/javascripts/discourse/admin-discourse-ai-plugin-route-map.js

+4
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,9 @@ export default {
2929
this.route("edit", { path: "/:id/edit" });
3030
}
3131
);
32+
33+
this.route("discourse-ai-features", { path: "ai-features" }, function () {
34+
this.route("edit", { path: "/:id/edit" });
35+
});
3236
},
3337
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import RestAdapter from "discourse/adapters/rest";
2+
3+
export default class AiFeatureAdapter extends RestAdapter {
4+
jsonMode = true;
5+
6+
basePath() {
7+
return "/admin/plugins/discourse-ai/";
8+
}
9+
10+
pathFor(store, type, findArgs) {
11+
// removes underscores which are implemented in base
12+
let path =
13+
this.basePath(store, type, findArgs) +
14+
store.pluralize(this.apiNameFor(type));
15+
return this.appendQueryParams(path, findArgs);
16+
}
17+
18+
apiNameFor() {
19+
return "ai-feature";
20+
}
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import RestModel from "discourse/models/rest";
2+
3+
export default class AiFeature extends RestModel {
4+
createProperties() {
5+
return this.getProperties(
6+
"id",
7+
"name",
8+
"ref",
9+
"description",
10+
"enable_setting",
11+
"persona",
12+
"persona_setting"
13+
);
14+
}
15+
}

assets/javascripts/initializers/admin-plugin-configuration-nav.js

+6
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ export default {
4141
route: "adminPlugins.show.discourse-ai-spam",
4242
description: "discourse_ai.spam.spam_description",
4343
},
44+
// TODO(@keegan / @roman): Uncomment this when structured output is merged
45+
// {
46+
// label: "discourse_ai.features.short_title",
47+
// route: "adminPlugins.show.discourse-ai-features",
48+
// description: "discourse_ai.features.description",
49+
// },
4450
]);
4551
});
4652
},

0 commit comments

Comments
 (0)