From 411da2ee884859b6ea1b8bf74d8c90665af716b1 Mon Sep 17 00:00:00 2001 From: awesomerobot Date: Wed, 4 Jun 2025 19:54:41 -0400 Subject: [PATCH 1/8] WIP: add features to persona list, and other style updates --- .../admin/ai_personas_controller.rb | 21 ++++++++++--- .../localized_ai_persona_serializer.rb | 8 ++++- .../components/ai-persona-list-editor.gjs | 31 ++++++++++++------- .../modules/ai-bot/common/ai-persona.scss | 25 +++++++++++++++ 4 files changed, 68 insertions(+), 17 deletions(-) diff --git a/app/controllers/discourse_ai/admin/ai_personas_controller.rb b/app/controllers/discourse_ai/admin/ai_personas_controller.rb index 2b57dda9a..5d00f8cdf 100644 --- a/app/controllers/discourse_ai/admin/ai_personas_controller.rb +++ b/app/controllers/discourse_ai/admin/ai_personas_controller.rb @@ -8,16 +8,25 @@ class AiPersonasController < ::Admin::AdminController before_action :find_ai_persona, only: %i[edit update destroy create_user] def index + features_by_persona_id = DiscourseAi::Features.features.group_by { |f| f[:persona]&.id } + ai_personas = - AiPersona.ordered.map do |persona| - # we use a special serializer here cause names and descriptions are - # localized for system personas - LocalizedAiPersonaSerializer.new(persona, root: false) - end + AiPersona + .ordered + .includes(:user, :uploads) + .map do |persona| + LocalizedAiPersonaSerializer.new( + persona, + root: false, + features_by_persona_id: features_by_persona_id, + ) + end + tools = DiscourseAi::Personas::Persona.all_available_tools.map do |tool| AiToolSerializer.new(tool, root: false) end + AiTool .where(enabled: true) .each do |tool| @@ -31,10 +40,12 @@ def index ), } end + llms = DiscourseAi::Configuration::LlmEnumerator.values_for_serialization( allowed_seeded_llm_ids: SiteSetting.ai_bot_allowed_seeded_models_map, ) + render json: { ai_personas: ai_personas, meta: { diff --git a/app/serializers/localized_ai_persona_serializer.rb b/app/serializers/localized_ai_persona_serializer.rb index 52ff044b9..5f7f328fb 100644 --- a/app/serializers/localized_ai_persona_serializer.rb +++ b/app/serializers/localized_ai_persona_serializer.rb @@ -3,6 +3,11 @@ class LocalizedAiPersonaSerializer < ApplicationSerializer root "ai_persona" + def initialize(object, options = {}) + @features_by_persona_id = options.delete(:features_by_persona_id) + super(object, options) + end + attributes :id, :name, :description, @@ -32,7 +37,8 @@ class LocalizedAiPersonaSerializer < ApplicationSerializer :allow_personal_messages, :force_default_llm, :response_format, - :examples + :examples, + :features has_one :user, serializer: BasicUserSerializer, embed: :object has_many :rag_uploads, serializer: UploadSerializer, embed: :object diff --git a/assets/javascripts/discourse/components/ai-persona-list-editor.gjs b/assets/javascripts/discourse/components/ai-persona-list-editor.gjs index 69cadec15..39dcd40e1 100644 --- a/assets/javascripts/discourse/components/ai-persona-list-editor.gjs +++ b/assets/javascripts/discourse/components/ai-persona-list-editor.gjs @@ -1,13 +1,12 @@ import Component from "@glimmer/component"; -import { fn } from "@ember/helper"; -import { on } from "@ember/modifier"; import { action } from "@ember/object"; import { LinkTo } from "@ember/routing"; import { service } from "@ember/service"; import DBreadcrumbsItem from "discourse/components/d-breadcrumbs-item"; +import DButton from "discourse/components/d-button"; import DPageSubheader from "discourse/components/d-page-subheader"; -import DToggleSwitch from "discourse/components/d-toggle-switch"; import concatClass from "discourse/helpers/concat-class"; +import icon from "discourse/helpers/d-icon"; import { popupAjaxError } from "discourse/lib/ajax-error"; import { i18n } from "discourse-i18n"; import AdminConfigAreaEmptyList from "admin/components/admin-config-area-empty-list"; @@ -61,17 +60,18 @@ export default class AiPersonaListEditor extends Component { {{i18n "discourse_ai.ai_persona.name"}} - {{i18n "discourse_ai.ai_persona.list.enabled"}} - + {{i18n "discourse_ai.features.short_title"}} {{#each @personas as |persona|}} + {{log persona}} @@ -79,6 +79,7 @@ export default class AiPersonaListEditor extends Component {
{{persona.name}} + {{#if persona.enabled}}{{icon "check"}}{{/if}}
@@ -86,12 +87,20 @@ export default class AiPersonaListEditor extends Component {
- - + + + {{#each persona.features as |feature|}} + {{log persona}} + + {{/each}} + + Date: Wed, 4 Jun 2025 20:01:10 -0400 Subject: [PATCH 2/8] remove log --- .../javascripts/discourse/components/ai-persona-list-editor.gjs | 2 -- 1 file changed, 2 deletions(-) diff --git a/assets/javascripts/discourse/components/ai-persona-list-editor.gjs b/assets/javascripts/discourse/components/ai-persona-list-editor.gjs index 39dcd40e1..064fba6a8 100644 --- a/assets/javascripts/discourse/components/ai-persona-list-editor.gjs +++ b/assets/javascripts/discourse/components/ai-persona-list-editor.gjs @@ -65,7 +65,6 @@ export default class AiPersonaListEditor extends Component { {{#each @personas as |persona|}} - {{log persona}} {{#each persona.features as |feature|}} - {{log persona}} Date: Thu, 5 Jun 2025 17:57:40 -0400 Subject: [PATCH 3/8] UX: add filters, card view option, additional styles --- .../components/ai-persona-list-editor.gjs | 223 ++++++++++++++++-- .../modules/ai-bot/common/ai-persona.scss | 116 ++++++++- config/locales/client.en.yml | 10 + 3 files changed, 328 insertions(+), 21 deletions(-) diff --git a/assets/javascripts/discourse/components/ai-persona-list-editor.gjs b/assets/javascripts/discourse/components/ai-persona-list-editor.gjs index 064fba6a8..c74a55c4a 100644 --- a/assets/javascripts/discourse/components/ai-persona-list-editor.gjs +++ b/assets/javascripts/discourse/components/ai-persona-list-editor.gjs @@ -1,19 +1,106 @@ import Component from "@glimmer/component"; +import { tracked } from "@glimmer/tracking"; +import { concat, fn, hash } from "@ember/helper"; import { action } from "@ember/object"; import { LinkTo } from "@ember/routing"; import { service } from "@ember/service"; import DBreadcrumbsItem from "discourse/components/d-breadcrumbs-item"; import DButton from "discourse/components/d-button"; import DPageSubheader from "discourse/components/d-page-subheader"; +import DSelect from "discourse/components/d-select"; +import DropdownMenu from "discourse/components/dropdown-menu"; +import FilterInput from "discourse/components/filter-input"; +import avatar from "discourse/helpers/avatar"; import concatClass from "discourse/helpers/concat-class"; import icon from "discourse/helpers/d-icon"; import { popupAjaxError } from "discourse/lib/ajax-error"; import { i18n } from "discourse-i18n"; import AdminConfigAreaEmptyList from "admin/components/admin-config-area-empty-list"; +import DMenu from "float-kit/components/d-menu"; import AiPersonaEditor from "./ai-persona-editor"; +const LAYOUT_BUTTONS = [ + { + id: "table", + label: "discourse_ai.layout.table", + icon: "discourse-table", + }, + { + id: "card", + label: "discourse_ai.layout.card", + icon: "table", + }, +]; + export default class AiPersonaListEditor extends Component { @service adminPluginNavManager; + @service keyValueStore; + @service capabilities; + + @tracked filterValue = ""; + @tracked featureFilter = "all"; + @tracked currentLayout = LAYOUT_BUTTONS[0]; + + constructor() { + super(...arguments); + const savedLayoutId = this.keyValueStore.get("ai-persona-list-layout"); + if (savedLayoutId) { + const found = LAYOUT_BUTTONS.find((b) => b.id === savedLayoutId); + if (found) { + this.currentLayout = found; + } + } + } + + get filteredPersonas() { + let personas = this.args.personas || []; + + // Filter by feature if not "all" + if (this.featureFilter !== "all") { + personas = personas.filter((persona) => + (persona.features || []).some( + (feature) => feature.name === this.featureFilter + ) + ); + } + + // Filter by search term if present + if (this.filterValue) { + const term = this.filterValue.toLowerCase(); + personas = personas.filter((persona) => { + const textMatches = + persona.name?.toLowerCase().includes(term) || + persona.description?.toLowerCase().includes(term); + + const featureMatches = (persona.features || []).some((feature) => + feature.name?.toLowerCase().includes(term) + ); + + return textMatches || featureMatches; + }); + } + + return personas; + } + + get featureFilterOptions() { + let features = []; + (this.args.personas || []).forEach((persona) => { + (persona.features || []).forEach((feature) => { + if (feature?.name && !features.includes(feature.name)) { + features.push(feature.name); + } + }); + }); + features.sort(); + return [ + { + value: "all", + label: i18n("discourse_ai.ai_persona.filters.all_features"), + }, + ...features.map((name) => ({ value: name, label: name })), + ]; + } @action async toggleEnabled(persona) { @@ -29,12 +116,47 @@ export default class AiPersonaListEditor extends Component { } } + @action + onNameFilterChange(event) { + this.filterValue = event.target?.value || ""; + } + + @action + onFeatureFilterChange(value) { + this.featureFilter = value; + } + + @action + resetAndFocus() { + this.filterValue = ""; + this.featureFilter = "all"; + document.querySelector(".admin-filter__input").focus(); + } + + @action + onRegisterApi(api) { + this.dMenu = api; + } + + @action + onLayoutSelect(layoutId) { + const found = LAYOUT_BUTTONS.find((b) => b.id === layoutId); + if (found) { + this.currentLayout = found; + this.keyValueStore.set({ + key: "ai-persona-list-layout", + value: layoutId, + }); + } + this.dMenu.close(); + } +