Skip to content
This repository was archived by the owner on Jul 22, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { action } from "@ember/object";
import { ajax } from "discourse/lib/ajax";
import DiscourseRoute from "discourse/routes/discourse";
import SiteSetting from "admin/models/site-setting";
Expand All @@ -24,4 +25,11 @@ export default class AdminPluginsShowDiscourseAiFeaturesEdit extends DiscourseRo

return currentFeature;
}

@action
willTransition() {
// site settings may amend if a feature is enabled or disabled, so refresh the model
// even on back button
this.router.refresh("adminPlugins.show.discourse-ai-features");
}
}
14 changes: 6 additions & 8 deletions app/controllers/discourse_ai/admin/ai_features_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@ def serialize_module(a_module)
def serialize_feature(feature)
{
name: feature.name,
persona: serialize_persona(persona_id_obj_hash[feature.persona_id]),
llm_model: {
id: feature.llm_model&.id,
name: feature.llm_model&.name,
},
personas: feature.persona_ids.map { |id| serialize_persona(persona_id_obj_hash[id]) },
llm_models:
feature.llm_models.map do |llm_model|
{ id: llm_model.id, name: llm_model.display_name }
end,
enabled: feature.enabled?,
}
end
Expand All @@ -57,9 +57,7 @@ def serialize_persona(persona)
def persona_id_obj_hash
@persona_id_obj_hash ||=
begin
setting_names = DiscourseAi::Configuration::Feature.all_persona_setting_names
ids = setting_names.map { |sn| SiteSetting.public_send(sn) }

ids = DiscourseAi::Configuration::Feature.all.map(&:persona_ids).flatten.uniq
AiPersona.where(id: ids).index_by(&:id)
end
end
Expand Down
282 changes: 195 additions & 87 deletions assets/javascripts/discourse/components/ai-features-list.gjs
Original file line number Diff line number Diff line change
@@ -1,98 +1,206 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { concat } from "@ember/helper";
import { gt } from "truth-helpers";
import { action } from "@ember/object";
import DButton from "discourse/components/d-button";
import { i18n } from "discourse-i18n";

const AiFeaturesList = <template>
<div class="ai-features-list">
{{#each @modules as |module|}}
<div class="ai-module" data-module-name={{module.module_name}}>
<div class="ai-module__header">
<div class="ai-module__module-title">
<h3>{{i18n
(concat "discourse_ai.features." module.module_name ".name")
}}</h3>
<DButton
class="edit"
@label="discourse_ai.features.edit"
@route="adminPlugins.show.discourse-ai-features.edit"
@routeModels={{module.id}}
/>
class ExpandableList extends Component {
@tracked isExpanded = false;

get maxItemsToShow() {
return this.args.maxItemsToShow ?? 5;
}

get hasMore() {
return this.args.items?.length > this.maxItemsToShow;
}

get visibleItems() {
if (!this.args.items) {
return [];
}
return this.isExpanded
? this.args.items
: this.args.items.slice(0, this.maxItemsToShow);
}

get remainingCount() {
return this.args.items?.length - this.maxItemsToShow;
}

get expandToggleLabel() {
if (this.isExpanded) {
return i18n("discourse_ai.features.collapse_list");
} else {
return i18n("discourse_ai.features.expand_list", {
count: this.remainingCount,
});
}
}

@action
toggleExpanded() {
this.isExpanded = !this.isExpanded;
}

<template>
{{#each this.visibleItems as |item index|}}
{{yield item index}}
{{/each}}

{{#if this.hasMore}}
<DButton
class="btn-flat btn-small ai-expanded-list__toggle-button"
@translatedLabel={{this.expandToggleLabel}}
@action={{this.toggleExpanded}}
/>
{{/if}}
</template>
}

export default class AiFeaturesList extends Component {
get sortedModules() {
return this.args.modules.sort((a, b) => {
const nameA = i18n(`discourse_ai.features.${a.module_name}.name`);
const nameB = i18n(`discourse_ai.features.${b.module_name}.name`);
return nameA.localeCompare(nameB);
});
}

@action
hasGroups(feature) {
return this.groupList(feature).length > 0;
}

@action
groupList(feature) {
const groups = [];
const groupIds = new Set();
if (feature.personas) {
feature.personas.forEach((persona) => {
if (persona.allowed_groups) {
persona.allowed_groups.forEach((group) => {
if (!groupIds.has(group.id)) {
groupIds.add(group.id);
groups.push(group);
}
});
}
});
}
return groups;
}

<template>
<div class="ai-features-list">
{{#each this.sortedModules as |module|}}
<div class="ai-module" data-module-name={{module.module_name}}>
<div class="ai-module__header">
<div class="ai-module__module-title">
<h3>{{i18n
(concat "discourse_ai.features." module.module_name ".name")
}}</h3>
<DButton
class="edit"
@label="discourse_ai.features.edit"
@route="adminPlugins.show.discourse-ai-features.edit"
@routeModels={{module.id}}
/>
</div>
<div>{{i18n
(concat
"discourse_ai.features." module.module_name ".description"
)
}}</div>
</div>
<div>{{i18n
(concat
"discourse_ai.features." module.module_name ".description"
)
}}</div>
</div>

<div class="admin-section-landing-wrapper ai-feature-cards">
{{#each module.features as |feature|}}
<div
class="admin-section-landing-item ai-feature-card"
data-feature-name={{feature.name}}
>
<div class="admin-section-landing-item__content">
<div class="ai-feature-card__feature-name">
{{i18n
(concat
"discourse_ai.features."
module.module_name
"."
feature.name
)
}}
{{#unless feature.enabled}}
<span>{{i18n "discourse_ai.features.disabled"}}</span>
{{/unless}}
</div>
<div class="ai-feature-card__persona">
<span>{{i18n "discourse_ai.features.persona"}}</span>
{{#if feature.persona}}
<DButton
class="btn-flat btn-small ai-feature-card__persona-button"
@translatedLabel={{feature.persona.name}}
@route="adminPlugins.show.discourse-ai-personas.edit"
@routeModels={{feature.persona.id}}
/>
{{else}}
{{i18n "discourse_ai.features.no_persona"}}
{{/if}}
</div>
<div class="ai-feature-card__llm">
<span>{{i18n "discourse_ai.features.llm"}}</span>
{{#if feature.llm_model.name}}
<DButton
class="btn-flat btn-small ai-feature-card__llm-button"
@translatedLabel={{feature.llm_model.name}}
@route="adminPlugins.show.discourse-ai-llms.edit"
@routeModels={{feature.llm_model.id}}
/>
{{else}}
{{i18n "discourse_ai.features.no_llm"}}
{{/if}}
</div>
{{#if feature.persona}}
<div class="ai-feature-card__groups">
<span>{{i18n "discourse_ai.features.groups"}}</span>
{{#if (gt feature.persona.allowed_groups.length 0)}}
<ul class="ai-feature-card__item-groups">
{{#each feature.persona.allowed_groups as |group|}}
<li>{{group.name}}</li>
{{/each}}
</ul>
<div class="admin-section-landing-wrapper ai-feature-cards">
{{#each module.features as |feature|}}
<div
class="admin-section-landing-item ai-feature-card"
data-feature-name={{feature.name}}
>
<div class="admin-section-landing-item__content">
<div class="ai-feature-card__feature-name">
{{i18n
(concat
"discourse_ai.features."
module.module_name
"."
feature.name
)
}}
{{#unless feature.enabled}}
<span>{{i18n "discourse_ai.features.disabled"}}</span>
{{/unless}}
</div>
<div class="ai-feature-card__persona">
<span>{{i18n
"discourse_ai.features.persona"
count=feature.personas.length
}}</span>
{{#if feature.personas}}
<ExpandableList
@items={{feature.personas}}
@maxItemsToShow={{5}}
as |persona|
>
<DButton
class="btn-flat btn-small ai-feature-card__persona-button"
@translatedLabel={{persona.name}}
@route="adminPlugins.show.discourse-ai-personas.edit"
@routeModels={{persona.id}}
/>
</ExpandableList>
{{else}}
{{i18n "discourse_ai.features.no_groups"}}
{{i18n "discourse_ai.features.no_persona"}}
{{/if}}
</div>
{{/if}}
<div class="ai-feature-card__llm">
{{#if feature.llm_models}}
<span>{{i18n
"discourse_ai.features.llm"
count=feature.llm_models.length
}}</span>
{{/if}}
{{#if feature.llm_models}}
<ExpandableList
@items={{feature.llm_models}}
@maxItemsToShow={{5}}
as |llm|
>
<DButton
class="btn-flat btn-small ai-feature-card__llm-button"
@translatedLabel={{llm.name}}
@route="adminPlugins.show.discourse-ai-llms.edit"
@routeModels={{llm.id}}
/>
</ExpandableList>
{{else}}
{{i18n "discourse_ai.features.no_llm"}}
{{/if}}
</div>
{{#if feature.personas}}
<div class="ai-feature-card__groups">
<span>{{i18n "discourse_ai.features.groups"}}</span>
{{#if (this.hasGroups feature)}}
<ul class="ai-feature-card__item-groups">
{{#each (this.groupList feature) as |group|}}
<li>{{group.name}}</li>
{{/each}}
</ul>
{{else}}
{{i18n "discourse_ai.features.no_groups"}}
{{/if}}
</div>
{{/if}}
</div>
</div>
</div>
{{/each}}
{{/each}}
</div>
</div>
</div>
{{/each}}
</div>
</template>;

export default AiFeaturesList;
{{/each}}
</div>
</template>
}
10 changes: 8 additions & 2 deletions assets/stylesheets/common/ai-features.scss
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,18 @@
background: var(--primary-very-low);
border: 1px solid var(--primary-low);
padding: 0.5rem;
display: block;
display: flex;
flex-direction: column;

&__llm,
&__persona,
&__groups {
font-size: var(--font-down-1-rem);
display: flex;
flex-flow: row wrap;
gap: 0.1em;
margin-top: 0.5rem;
align-items: center;
}

&__persona {
Expand All @@ -36,7 +42,7 @@

&__persona-button,
&__llm-button {
padding-left: 0;
padding-left: 0.2em;
}

&__groups {
Expand Down
16 changes: 14 additions & 2 deletions config/locales/client.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -186,13 +186,25 @@ en:
description: "These are the AI features available to visitors on your site. These can be configured to use specific personas and LLMs, and can be access controlled by groups."
back: "Back"
disabled: "(disabled)"
persona: "Persona:"
persona:
one: "Persona:"
other: "Personas:"
groups: "Groups:"
llm: "LLM:"
llm:
one: "LLM:"
other: "LLMs:"
no_llm: "No LLM selected"
no_persona: "Not set"
no_groups: "None"
edit: "Edit"
expand_list:
one: "(%{count} more)"
other: "(%{count} more)"
collapse_list: "(show less)"
bot:
bot: "Chatbot"
name: "Bot"
description: "A chat bot that can answer questions and assist users in private messagges, forum and in chat"
nav:
configured: "Configured"
unconfigured: "Unconfigured"
Expand Down
Loading
Loading