UX: Applying more admin UI guidelines (#956)

This commit applies further admin UI guidelines, now that they have been more
fleshed out in core, to the AI admin UI:

* Tools
* LLMs
* Personas

The changes include but are not limited to:

* Applying the table CSS classes, for desktop and mobile
* Adding a description and learn more link for each tab
* Adding an empty list placeholder with CTA using `AdminConfigAreaEmptyList`
* Replacing custom headings with `AdminPageSubheader`
This commit is contained in:
Martin Brennan 2024-11-27 13:34:56 +10:00 committed by GitHub
parent 7a094cde18
commit 2f7895bb91
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 177 additions and 191 deletions

View File

@ -1,5 +1,7 @@
<AiToolEditor
@tools={{this.allTools}}
@model={{this.model}}
@presets={{this.presets}}
/>
<section class="ai-persona-tool-editor__current admin-detail pull-left">
<AiToolEditor
@tools={{this.allTools}}
@model={{this.model}}
@presets={{this.presets}}
/>
</section>

View File

@ -1,5 +1,7 @@
<AiToolEditor
@tools={{this.allTools}}
@model={{this.model}}
@presets={{this.presets}}
/>
<section class="ai-persona-tool-editor__current admin-detail pull-left">
<AiToolEditor
@tools={{this.allTools}}
@model={{this.model}}
@presets={{this.presets}}
/>
</section>

View File

@ -4,7 +4,6 @@ import { action } from "@ember/object";
import { LinkTo } from "@ember/routing";
import { service } from "@ember/service";
import DBreadcrumbsItem from "discourse/components/d-breadcrumbs-item";
import icon from "discourse-common/helpers/d-icon";
import i18n from "discourse-common/helpers/i18n";
import I18n from "discourse-i18n";
import AdminPageSubheader from "admin/components/admin-page-subheader";
@ -124,8 +123,10 @@ export default class AiLlmsListEditor extends Component {
<section class="ai-llms-list-editor__configured">
<AdminPageSubheader
@titleLabel="discourse_ai.llms.configured.title"
@descriptionLabel="discourse_ai.llms.preconfigured.description"
@learnMoreUrl="https://meta.discourse.org/t/discourse-ai-large-language-model-llm-settings-page/319903"
/>
<table>
<table class="d-admin-table">
<thead>
<tr>
<th>{{i18n "discourse_ai.llms.display_name"}}</th>
@ -135,8 +136,11 @@ export default class AiLlmsListEditor extends Component {
</thead>
<tbody>
{{#each @llms as |llm|}}
<tr data-llm-id={{llm.name}} class="ai-llm-list__row">
<td class="column-name">
<tr
data-llm-id={{llm.name}}
class="ai-llm-list__row d-admin-row__content"
>
<td class="d-admin-row__overview">
<h3>{{llm.display_name}}</h3>
<p>
{{this.modelDescription llm}}
@ -149,18 +153,20 @@ export default class AiLlmsListEditor extends Component {
</ul>
{{/if}}
</td>
<td class="column-provider">
<td class="d-admin-row__detail">
<div class="d-admin-row__mobile-label">
{{i18n "discourse_ai.llms.provider"}}
</div>
{{i18n
(concat "discourse_ai.llms.providers." llm.provider)
}}
</td>
<td class="column-edit">
<td class="d-admin-row__controls">
<LinkTo
@route="adminPlugins.show.discourse-ai-llms.show"
class="btn btn-default"
class="btn btn-default btn-small ai-llm-list__edit-button"
@model={{llm.id}}
>
{{icon "wrench"}}
<div class="d-button-label">
{{i18n "discourse_ai.llms.edit"}}
</div>
@ -173,7 +179,17 @@ export default class AiLlmsListEditor extends Component {
</section>
{{/if}}
<section class="ai-llms-list-editor__templates">
<AdminPageSubheader @titleLabel={{this.preconfiguredTitle}} />
<AdminPageSubheader
@titleLabel={{this.preconfiguredTitle}}
@descriptionLabel={{unless
this.hasLlmElements
"discourse_ai.llms.preconfigured.description"
}}
@learnMoreUrl={{unless
this.hasLlmElements
"https://meta.discourse.org/t/discourse-ai-large-language-model-llm-settings-page/319903"
}}
/>
<AdminSectionLandingWrapper
class="ai-llms-list-editor__templates-list"

View File

@ -1,5 +1,4 @@
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";
@ -9,26 +8,13 @@ import DBreadcrumbsItem from "discourse/components/d-breadcrumbs-item";
import DToggleSwitch from "discourse/components/d-toggle-switch";
import concatClass from "discourse/helpers/concat-class";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { cook } from "discourse/lib/text";
import icon from "discourse-common/helpers/d-icon";
import i18n from "discourse-common/helpers/i18n";
import I18n from "discourse-i18n";
import AdminConfigAreaEmptyList from "admin/components/admin-config-area-empty-list";
import AdminPageSubheader from "admin/components/admin-page-subheader";
import AiPersonaEditor from "./ai-persona-editor";
export default class AiPersonaListEditor extends Component {
@service adminPluginNavManager;
@tracked _noPersonaText = null;
get noPersonaText() {
if (this._noPersonaText === null) {
const raw = I18n.t("discourse_ai.ai_persona.no_persona_selected");
cook(raw).then((result) => {
this._noPersonaText = result;
});
}
return this._noPersonaText;
}
@action
async toggleEnabled(persona) {
@ -53,74 +39,76 @@ export default class AiPersonaListEditor extends Component {
{{#if @currentPersona}}
<AiPersonaEditor @model={{@currentPersona}} @personas={{@personas}} />
{{else}}
<div class="ai-persona-list-editor__header">
<h3>{{i18n "discourse_ai.ai_persona.short_title"}}</h3>
{{#unless @currentPersona.isNew}}
<LinkTo
<AdminPageSubheader
@titleLabel="discourse_ai.ai_persona.short_title"
@descriptionLabel="discourse_ai.ai_persona.persona_description"
@learnMoreUrl="https://meta.discourse.org/t/ai-bot-personas/306099"
>
<:actions as |actions|>
<actions.Primary
@label="discourse_ai.ai_persona.new"
@route="adminPlugins.show.discourse-ai-personas.new"
class="btn btn-small btn-primary"
>
{{icon "plus"}}
<span>{{I18n.t "discourse_ai.ai_persona.new"}}</span>
</LinkTo>
{{/unless}}
</div>
@icon="plus"
class="ai-persona-list-editor__new-button"
/>
</:actions>
</AdminPageSubheader>
<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-personas.show"
@model={{persona}}
class="btn btn-text btn-small"
>{{i18n "discourse_ai.ai_persona.edit"}} </LinkTo>
</td>
{{#if @personas}}
<table class="content-list ai-persona-list-editor d-admin-table">
<thead>
<tr>
<th>{{i18n "discourse_ai.ai_persona.name"}}</th>
<th>{{i18n "discourse_ai.ai_persona.enabled"}}</th>
<th></th>
</tr>
{{/each}}
</tbody>
</table>
</thead>
<tbody>
{{#each @personas as |persona|}}
<tr
data-persona-id={{persona.id}}
class={{concatClass
"ai-persona-list__row d-admin-row__content"
(if persona.priority "priority")
}}
>
<td class="d-admin-row__overview">
<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 class="d-admin-row__detail">
<DToggleSwitch
@state={{persona.enabled}}
{{on "click" (fn this.toggleEnabled persona)}}
/>
</td>
<td class="d-admin-row__controls">
<LinkTo
@route="adminPlugins.show.discourse-ai-personas.show"
@model={{persona}}
class="btn btn-text btn-small"
>{{i18n "discourse_ai.ai_persona.edit"}} </LinkTo>
</td>
</tr>
{{/each}}
</tbody>
</table>
{{else}}
<AdminConfigAreaEmptyList
@ctaLabel="discourse_ai.ai_persona.new"
@ctaRoute="adminPlugins.show.discourse-ai-personas.new"
@ctaClass="ai-persona-list-editor__empty-new-button"
@emptyLabel="discourse_ai.ai_persona.no_personas"
/>
{{/if}}
{{/if}}
</section>
</template>

View File

@ -2,9 +2,10 @@ import Component from "@glimmer/component";
import { LinkTo } from "@ember/routing";
import { service } from "@ember/service";
import DBreadcrumbsItem from "discourse/components/d-breadcrumbs-item";
import icon from "discourse-common/helpers/d-icon";
import i18n from "discourse-common/helpers/i18n";
import I18n from "discourse-i18n";
import AdminConfigAreaEmptyList from "admin/components/admin-config-area-empty-list";
import AdminPageSubheader from "admin/components/admin-page-subheader";
export default class AiToolListEditor extends Component {
@service adminPluginNavManager;
@ -15,44 +16,64 @@ export default class AiToolListEditor extends Component {
@label={{i18n "discourse_ai.tools.short_title"}}
/>
<section class="ai-tool-list-editor__current admin-detail pull-left">
<div class="ai-tool-list-editor__header">
<h3>{{I18n.t "discourse_ai.tools.short_title"}}</h3>
<LinkTo
@route="adminPlugins.show.discourse-ai-tools.new"
class="btn btn-small btn-primary ai-tool-list-editor__new-button"
>
{{icon "plus"}}
<span>{{I18n.t "discourse_ai.tools.new"}}</span>
</LinkTo>
</div>
<AdminPageSubheader
@titleLabel="discourse_ai.tools.short_title"
@learnMoreUrl="https://meta.discourse.org/t/ai-bot-custom-tools/314103"
@descriptionLabel="discourse_ai.tools.subheader_description"
>
<:actions as |actions|>
<actions.Primary
@label="discourse_ai.tools.new"
@route="adminPlugins.show.discourse-ai-tools.new"
@icon="plus"
class="ai-tool-list-editor__new-button"
/>
</:actions>
</AdminPageSubheader>
<table class="content-list ai-tool-list-editor">
<tbody>
{{#each @tools as |tool|}}
<tr data-tool-id={{tool.id}} class="ai-tool-list__row">
<td>
<div class="ai-tool-list__name-with-description">
<div class="ai-tool-list__name">
<strong>
{{tool.name}}
</strong>
{{#if @tools}}
<table class="d-admin-table ai-tool-list-editor">
<thead>
<th>{{i18n "discourse_ai.tools.name"}}</th>
<th></th>
</thead>
<tbody>
{{#each @tools as |tool|}}
<tr
data-tool-id={{tool.id}}
class="ai-tool-list__row d-admin-row__content"
>
<td class="d-admin-row__overview">
<div class="ai-tool-list__name-with-description">
<div class="ai-tool-list__name">
<strong>
{{tool.name}}
</strong>
</div>
<div class="ai-tool-list__description">
{{tool.description}}
</div>
</div>
<div class="ai-tool-list__description">
{{tool.description}}
</div>
</div>
</td>
<td>
<LinkTo
@route="adminPlugins.show.discourse-ai-tools.show"
@model={{tool}}
class="btn btn-text btn-small"
>{{I18n.t "discourse_ai.tools.edit"}}</LinkTo>
</td>
</tr>
{{/each}}
</tbody>
</table>
</td>
<td class="d-admin-row__controls">
<LinkTo
@route="adminPlugins.show.discourse-ai-tools.show"
@model={{tool}}
class="btn btn-text btn-small"
>{{I18n.t "discourse_ai.tools.edit"}}</LinkTo>
</td>
</tr>
{{/each}}
</tbody>
</table>
{{else}}
<AdminConfigAreaEmptyList
@ctaLabel="discourse_ai.tools.new"
@ctaRoute="adminPlugins.show.discourse-ai-tools.new"
@ctaClass="ai-tool-list-editor__empty-new-button"
@emptyLabel="discourse_ai.tools.no_tools"
/>
{{/if}}
</section>
</template>
}

View File

@ -67,43 +67,6 @@
}
.ai-llms-list-editor__configured {
p {
margin: 0;
color: var(--primary-high);
@include breakpoint("mobile-extra-large") {
display: none;
}
}
table {
th {
white-space: nowrap;
}
tr:hover {
background: transparent;
}
td {
padding: 1em 0.5em;
}
}
.column-name {
width: 100%;
}
.column-edit {
text-align: right;
@include breakpoint("mobile-extra-large") {
.d-button-label {
display: none;
}
.d-icon {
margin: 0;
}
}
}
.d-toggle-switch {
justify-content: center;
}
@ -129,7 +92,7 @@
}
}
p {
.admin-section-landing-item__description {
color: var(--primary-high);
margin: 0.25em 0 0.5em;
line-height: var(--line-height-large);

View File

@ -173,6 +173,7 @@ en:
allowed_groups: "Allowed groups"
confirm_delete: "Are you sure you want to delete this persona?"
new: "New persona"
no_personas: "You have not created any personas yet"
title: "Personas"
short_title: "Personas"
delete: "Delete"
@ -185,17 +186,7 @@ en:
tool_options: "Tool options"
rag_conversation_chunks: "Search conversation chunks"
rag_conversation_chunks_help: "The number of chunks to use for the RAG model searches. Increase to increase the amount of context the AI can use."
what_are_personas: "What are Personas?"
no_persona_selected: |
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 personas?
With 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, personas give you the flexibility to do so.
#### Group-specific access to 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.
persona_description: "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."
rag:
options:
@ -219,8 +210,10 @@ en:
tools:
back: "Back"
short_title: "Tools"
new: "New tool"
no_tools: "You have not created any tools yet"
name: "Name"
subheader_description: "Tools extend the capabilities of AI bots with user-defined JavaScript functions."
new: "New tool"
name_help: "The unique name of the tool as used by the language model"
description: "Description"
description_help: "A clear description of the tool's purpose for the language model"
@ -303,6 +296,7 @@ en:
preconfigured:
title_no_llms: "Select a template to get started"
title: "Unconfigured LLM templates"
description: "LLMs (Large Language Models) are AI tools optimized for tasks like summarizing content, generating reports, automating customer interactions, and facilitating forum moderation and insights."
fake: "Manual configuration"
button: "Set up"
next:

View File

@ -11,7 +11,7 @@ RSpec.describe "Admin AI persona configuration", type: :system, js: true do
it "allows creation of a persona" do
visit "/admin/plugins/discourse-ai/ai-personas"
find(".ai-persona-list-editor__header .btn-primary").click()
find(".ai-persona-list-editor__new-button").click()
find(".ai-persona-editor__name").set("Test Persona")
find(".ai-persona-editor__description").fill_in(with: "I am a test persona")
find(".ai-persona-editor__system_prompt").fill_in(with: "You are a helpful bot")

View File

@ -76,14 +76,14 @@ RSpec.describe "Managing LLM configurations", type: :system, js: true do
it "shows the provider as CDCK in the UI" do
visit "/admin/plugins/discourse-ai/ai-llms"
expect(page).to have_css(
"[data-llm-id='cdck-hosted'] .column-provider",
"[data-llm-id='cdck-hosted']",
text: I18n.t("js.discourse_ai.llms.providers.CDCK"),
)
end
it "shows an info alert to the user about the seeded LLM" do
visit "/admin/plugins/discourse-ai/ai-llms"
find("[data-llm-id='#{llm_model.name}'] .column-edit .btn").click()
find("[data-llm-id='#{llm_model.name}'] .ai-llm-list__edit-button").click()
expect(page).to have_css(
".alert.alert-info",
text: I18n.t("js.discourse_ai.llms.seeded_warning"),
@ -92,7 +92,7 @@ RSpec.describe "Managing LLM configurations", type: :system, js: true do
it "limits and shows disabled inputs for the seeded LLM" do
visit "/admin/plugins/discourse-ai/ai-llms"
find("[data-llm-id='cdck-hosted'] .column-edit .btn").click()
find("[data-llm-id='cdck-hosted'] .ai-llm-list__edit-button").click()
expect(page).to have_css(".ai-llm-editor__display-name[disabled]")
expect(page).to have_css(".ai-llm-editor__name[disabled]")
expect(page).to have_css(".ai-llm-editor__provider.is-disabled")