FEATURE: Improve admin plugin UI and use new plugins show route (#512)

This commit changes Discourse AI's admin plugin page to use the new plugin
show route. The UI for persona editing has also been improved for consistency,
and other plugin UIs will follow suit:

Settings for the plugin are now listed in the plugin UI and can be changed
from there directly after core PR discourse/discourse#26154 is merged.

See also:

* https://github.com/discourse/discourse/pull/26024
* https://github.com/discourse/discourse/pull/26154
* https://github.com/discourse/discourse/pull/26254
This commit is contained in:
Martin Brennan 2024-03-21 14:29:56 +10:00 committed by GitHub
parent 5cac47a30a
commit fb0d56324f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 176 additions and 67 deletions

View File

@ -1,10 +1,10 @@
export default { export default {
resource: "admin.adminPlugins", resource: "admin.adminPlugins.show",
path: "/plugins", path: "/plugins",
map() { map() {
this.route("discourse-ai", function () { this.route("discourse-ai", { path: "/" }, function () {
this.route("ai-personas", function () { this.route("ai-personas", function () {
this.route("new"); this.route("new");
this.route("show", { path: "/:id" }); this.route("show", { path: "/:id" });

View File

@ -8,6 +8,7 @@ import didUpdate from "@ember/render-modifiers/modifiers/did-update";
import { LinkTo } from "@ember/routing"; import { LinkTo } from "@ember/routing";
import { later } from "@ember/runloop"; import { later } from "@ember/runloop";
import { inject as service } from "@ember/service"; import { inject as service } from "@ember/service";
import BackButton from "discourse/components/back-button";
import DButton from "discourse/components/d-button"; import DButton from "discourse/components/d-button";
import Textarea from "discourse/components/d-textarea"; import Textarea from "discourse/components/d-textarea";
import DToggleSwitch from "discourse/components/d-toggle-switch"; import DToggleSwitch from "discourse/components/d-toggle-switch";
@ -58,7 +59,7 @@ export default class PersonaEditor extends Component {
if (isNew) { if (isNew) {
this.args.personas.addObject(this.args.model); this.args.personas.addObject(this.args.model);
this.router.transitionTo( this.router.transitionTo(
"adminPlugins.discourse-ai.ai-personas.show", "adminPlugins.show.discourse-ai.ai-personas.show",
this.args.model this.args.model
); );
} else { } else {
@ -109,7 +110,7 @@ export default class PersonaEditor extends Component {
return this.args.model.destroyRecord().then(() => { return this.args.model.destroyRecord().then(() => {
this.args.personas.removeObject(this.args.model); this.args.personas.removeObject(this.args.model);
this.router.transitionTo( this.router.transitionTo(
"adminPlugins.discourse-ai.ai-personas.index" "adminPlugins.show.discourse-ai.ai-personas.index"
); );
}); });
}, },
@ -180,6 +181,10 @@ export default class PersonaEditor extends Component {
} }
<template> <template>
<BackButton
@route="adminPlugins.show.discourse-ai.ai-personas"
@label="discourse_ai.ai_persona.back"
/>
<form <form
class="form-horizontal ai-persona-editor" class="form-horizontal ai-persona-editor"
{{didUpdate this.updateModel @model.id}} {{didUpdate this.updateModel @model.id}}

View File

@ -1,9 +1,15 @@
import Component from "@glimmer/component"; import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking"; 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 { LinkTo } from "@ember/routing";
import DToggleSwitch from "discourse/components/d-toggle-switch";
import concatClass from "discourse/helpers/concat-class"; import concatClass from "discourse/helpers/concat-class";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { cook } from "discourse/lib/text"; import { cook } from "discourse/lib/text";
import icon from "discourse-common/helpers/d-icon"; import icon from "discourse-common/helpers/d-icon";
import i18n from "discourse-common/helpers/i18n";
import I18n from "discourse-i18n"; import I18n from "discourse-i18n";
import AiPersonaEditor from "./ai-persona-editor"; import AiPersonaEditor from "./ai-persona-editor";
@ -21,45 +27,93 @@ export default class AiPersonaListEditor extends Component {
return this._noPersonaText; return this._noPersonaText;
} }
@action
async toggleEnabled(persona) {
const oldValue = persona.enabled;
const newValue = !oldValue;
try {
persona.set("enabled", newValue);
await persona.save();
} catch (err) {
persona.set("enabled", oldValue);
popupAjaxError(err);
}
}
<template> <template>
<div class="ai-persona-list-editor__header"> <section class="ai-persona-list-editor__current admin-detail pull-left">
<h3>{{I18n.t "discourse_ai.ai_persona.title"}}</h3>
{{#unless @currentPersona.isNew}}
<LinkTo
@route="adminPlugins.discourse-ai.ai-personas.new"
class="btn btn-primary"
>
{{icon "plus"}}
<span>{{I18n.t "discourse_ai.ai_persona.new"}}</span>
</LinkTo>
{{/unless}}
</div>
<div class="content-list ai-persona-list-editor">
<ul>
{{#each @personas as |persona|}}
<li
class={{concatClass
(if persona.enabled "" "diabled")
(if persona.priority "priority")
}}
>
<LinkTo
@route="adminPlugins.discourse-ai.ai-personas.show"
current-when="true"
@model={{persona}}
>{{persona.name}}
</LinkTo>
</li>
{{/each}}
</ul>
</div>
<section class="ai-persona-list-editor__current content-body">
{{#if @currentPersona}} {{#if @currentPersona}}
<AiPersonaEditor @model={{@currentPersona}} @personas={{@personas}} /> <AiPersonaEditor @model={{@currentPersona}} @personas={{@personas}} />
{{else}} {{else}}
<div class="ai-persona-list-editor__empty"> <div class="ai-persona-list-editor__header">
{{this.noPersonaText}} <h3>{{i18n "discourse_ai.ai_persona.short_title"}}</h3>
{{#unless @currentPersona.isNew}}
<LinkTo
@route="adminPlugins.show.discourse-ai.ai-personas.new"
class="btn btn-small btn-primary"
>
{{icon "plus"}}
<span>{{I18n.t "discourse_ai.ai_persona.new"}}</span>
</LinkTo>
{{/unless}}
</div> </div>
<div class="ai-persona-list-editor__empty">
<details class="details__boxed">
<summary>{{i18n
"discourse_ai.ai_persona.what_are_personas"
}}</summary>
{{this.noPersonaText}}
</details>
</div>
<table class="content-list ai-persona-list-editor">
<thead>
<tr>
<th>{{i18n "discourse_ai.ai_persona.name"}}</th>
<th>{{i18n "discourse_ai.ai_persona.enabled"}}</th>
<th></th>
</tr>
</thead>
<tbody>
{{#each @personas as |persona|}}
<tr
data-persona-id={{persona.id}}
class={{concatClass
"ai-persona-list__row"
(if persona.priority "priority")
}}
>
<td>
<div class="ai-persona-list__name-with-description">
<div class="ai-persona-list__name">
<strong>
{{persona.name}}
</strong>
</div>
<div class="ai-persona-list__description">
{{persona.description}}
</div>
</div>
</td>
<td>
<DToggleSwitch
@state={{persona.enabled}}
{{on "click" (fn this.toggleEnabled persona)}}
/>
</td>
<td>
<LinkTo
@route="adminPlugins.show.discourse-ai.ai-personas.show"
@model={{persona}}
class="btn btn-text btn-small"
>{{i18n "discourse_ai.ai_persona.edit"}} </LinkTo>
</td>
</tr>
{{/each}}
</tbody>
</table>
{{/if}} {{/if}}
</section> </section>
</template> </template>

View File

@ -1,9 +0,0 @@
import { inject as service } from "@ember/service";
import DiscourseRoute from "discourse/routes/discourse";
export default DiscourseRoute.extend({
router: service(),
beforeModel() {
this.router.transitionTo("adminPlugins.discourse-ai.ai-personas");
},
});

View File

@ -2,6 +2,6 @@ import DiscourseRoute from "discourse/routes/discourse";
export default DiscourseRoute.extend({ export default DiscourseRoute.extend({
model() { model() {
return this.modelFor("adminPlugins.discourse-ai.ai-personas"); return this.modelFor("adminPlugins.show.discourse-ai.ai-personas");
}, },
}); });

View File

@ -1,10 +1,10 @@
import { AUTO_GROUPS } from "discourse/lib/constants";
import DiscourseRoute from "discourse/routes/discourse"; import DiscourseRoute from "discourse/routes/discourse";
export default DiscourseRoute.extend({ export default DiscourseRoute.extend({
async model() { async model() {
const record = this.store.createRecord("ai-persona"); const record = this.store.createRecord("ai-persona");
// TL0 record.set("allowed_group_ids", [AUTO_GROUPS.trust_level_0.id]);
record.set("allowed_group_ids", [10]);
return record; return record;
}, },
@ -12,7 +12,7 @@ export default DiscourseRoute.extend({
this._super(controller, model); this._super(controller, model);
controller.set( controller.set(
"allPersonas", "allPersonas",
this.modelFor("adminPlugins.discourse-ai.ai-personas") this.modelFor("adminPlugins.show.discourse-ai.ai-personas")
); );
}, },
}); });

View File

@ -2,7 +2,9 @@ import DiscourseRoute from "discourse/routes/discourse";
export default DiscourseRoute.extend({ export default DiscourseRoute.extend({
async model(params) { async model(params) {
const allPersonas = this.modelFor("adminPlugins.discourse-ai.ai-personas"); const allPersonas = this.modelFor(
"adminPlugins.show.discourse-ai.ai-personas"
);
const id = parseInt(params.id, 10); const id = parseInt(params.id, 10);
return allPersonas.findBy("id", id); return allPersonas.findBy("id", id);
}, },
@ -11,7 +13,7 @@ export default DiscourseRoute.extend({
this._super(controller, model); this._super(controller, model);
controller.set( controller.set(
"allPersonas", "allPersonas",
this.modelFor("adminPlugins.discourse-ai.ai-personas") this.modelFor("adminPlugins.show.discourse-ai.ai-personas")
); );
}, },
}); });

View File

@ -1,7 +1,7 @@
import DiscourseRoute from "discourse/routes/discourse"; import DiscourseRoute from "discourse/routes/discourse";
export default DiscourseRoute.extend({ export default class DiscourseAiAiPersonasRoute extends DiscourseRoute {
async model() { model() {
return this.store.findAll("ai-persona"); return this.store.findAll("ai-persona");
}, }
}); }

View File

@ -0,0 +1,12 @@
import { inject as service } from "@ember/service";
import DiscourseRoute from "discourse/routes/discourse";
export default class DiscourseAiIndexRoute extends DiscourseRoute {
@service router;
beforeModel() {
return this.router.transitionTo(
"adminPlugins.show.discourse-ai.ai-personas.index"
);
}
}

View File

@ -0,0 +1 @@
<AiPersonaListEditor @personas={{this.model}} />

View File

@ -0,0 +1 @@
<AiPersonaListEditor @personas={{this.model}} />

View File

@ -0,0 +1,26 @@
import { PLUGIN_NAV_MODE_TOP } from "discourse/lib/admin-plugin-config-nav";
import { withPluginApi } from "discourse/lib/plugin-api";
export default {
name: "discourse-ai-admin-plugin-configuration-nav",
initialize(container) {
const currentUser = container.lookup("service:current-user");
if (!currentUser || !currentUser.admin) {
return;
}
withPluginApi("1.1.0", (api) => {
api.addAdminPluginConfigurationNav("discourse-ai", PLUGIN_NAV_MODE_TOP, [
{
label: "admin.plugins.change_settings_short",
route: "adminPlugins.show.settings",
},
{
label: "discourse_ai.ai_persona.short_title",
route: "adminPlugins.show.discourse-ai.ai-personas",
},
]);
});
},
};

View File

@ -1,10 +1,19 @@
.admin-contents .ai-persona-list-editor {
margin-top: 0;
}
.ai-persona-list-editor { .ai-persona-list-editor {
&__header { &__header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 10px; margin: 0 0 1em 0;
h3 {
margin: 0;
}
} }
&__current { &__current {
padding-left: 20px; padding-left: 20px;
} }

View File

@ -0,0 +1,7 @@
.ai-persona-editor {
&__system_prompt,
&__description,
.select-kit.multi-select {
width: 100%;
}
}

View File

@ -114,7 +114,9 @@ en:
select_option: "Select an option..." select_option: "Select an option..."
ai_persona: ai_persona:
back: Back
name: Name name: Name
edit: Edit
description: Description description: Description
no_llm_selected: "No language model selected" no_llm_selected: "No language model selected"
max_context_posts: "Max Context Posts" max_context_posts: "Max Context Posts"
@ -129,12 +131,13 @@ en:
system_prompt: System Prompt system_prompt: System Prompt
save: Save save: Save
saved: AI Persona Saved saved: AI Persona Saved
enabled: Enabled enabled: "Enabled?"
commands: Enabled Commands commands: Enabled Commands
allowed_groups: Allowed Groups allowed_groups: Allowed Groups
confirm_delete: Are you sure you want to delete this persona? confirm_delete: Are you sure you want to delete this persona?
new: New new: "New Persona"
title: "AI Personas" title: "AI Personas"
short_title: "Personas"
delete: Delete delete: Delete
temperature: Temperature temperature: Temperature
temperature_help: Temperature to use for the LLM, increase to increase creativity (leave empty to use model default, generally a value from 0.0 to 2.0) temperature_help: Temperature to use for the LLM, increase to increase creativity (leave empty to use model default, generally a value from 0.0 to 2.0)
@ -143,16 +146,15 @@ en:
priority: Priority priority: Priority
priority_help: Priority personas are displayed to users at the top of the persona list. If multiple personas have priority, they will be sorted alphabetically. priority_help: Priority personas are displayed to users at the top of the persona list. If multiple personas have priority, they will be sorted alphabetically.
command_options: "Command Options" command_options: "Command Options"
what_are_personas: "What are AI Personas?"
no_persona_selected: | no_persona_selected: |
## What are AI Personas?
AI Personas are a powerful feature that allows you to customize the behavior of the AI engine in your Discourse forum. They act as a 'system message' that guides the AI's responses and interactions, helping to create a more personalized and engaging user experience. AI Personas are a powerful feature that allows you to customize the behavior of the AI engine in your Discourse forum. They act as a 'system message' that guides the AI's responses and interactions, helping to create a more personalized and engaging user experience.
## Why use AI Personas? #### Why use AI Personas?
With AI Personas, you can tailor the AI's behavior to better fit the context and tone of your forum. Whether you want the AI to be more formal for a professional setting, more casual for a community forum, or even embody a specific character for a role-playing game, AI Personas give you the flexibility to do so. With AI Personas, you can tailor the AI's behavior to better fit the context and tone of your forum. Whether you want the AI to be more formal for a professional setting, more casual for a community forum, or even embody a specific character for a role-playing game, AI Personas give you the flexibility to do so.
## Group-Specific Access to AI Personas #### Group-Specific Access to AI Personas
Moreover, you can set it up so that certain user groups have access to specific personas. This means you can have different AI behaviors for different sections of your forum, further enhancing the diversity and richness of your community's interactions. Moreover, you can set it up so that certain user groups have access to specific personas. This means you can have different AI behaviors for different sections of your forum, further enhancing the diversity and richness of your community's interactions.

View File

@ -35,8 +35,6 @@ Discourse::Application.routes.draw do
:constraints => StaffConstraint.new :constraints => StaffConstraint.new
scope "/admin/plugins/discourse-ai", constraints: AdminConstraint.new do scope "/admin/plugins/discourse-ai", constraints: AdminConstraint.new do
get "/", to: redirect("/admin/plugins/discourse-ai/ai-personas")
resources :ai_personas, resources :ai_personas,
only: %i[index create show update destroy], only: %i[index create show update destroy],
path: "ai-personas", path: "ai-personas",

View File

@ -17,6 +17,7 @@ register_asset "stylesheets/modules/ai-helper/common/ai-helper.scss"
register_asset "stylesheets/modules/ai-bot/common/bot-replies.scss" register_asset "stylesheets/modules/ai-bot/common/bot-replies.scss"
register_asset "stylesheets/modules/ai-bot/common/ai-persona.scss" register_asset "stylesheets/modules/ai-bot/common/ai-persona.scss"
register_asset "stylesheets/modules/ai-bot/mobile/ai-persona.scss", :mobile
register_asset "stylesheets/modules/embeddings/common/semantic-related-topics.scss" register_asset "stylesheets/modules/embeddings/common/semantic-related-topics.scss"
register_asset "stylesheets/modules/embeddings/common/semantic-search.scss" register_asset "stylesheets/modules/embeddings/common/semantic-search.scss"
@ -38,7 +39,7 @@ after_initialize do
require_relative "discourse_automation/llm_triage" require_relative "discourse_automation/llm_triage"
require_relative "discourse_automation/llm_report" require_relative "discourse_automation/llm_report"
add_admin_route "discourse_ai.title", "discourse-ai" add_admin_route("discourse_ai.title", "discourse-ai", { use_new_show_route: true })
[ [
DiscourseAi::Embeddings::EntryPoint.new, DiscourseAi::Embeddings::EntryPoint.new,