diff --git a/admin/assets/javascripts/discourse/routes/admin-plugins-show-discourse-ai-features-edit.js b/admin/assets/javascripts/discourse/routes/admin-plugins-show-discourse-ai-features-edit.js index 82e90e74..c6d8ae41 100644 --- a/admin/assets/javascripts/discourse/routes/admin-plugins-show-discourse-ai-features-edit.js +++ b/admin/assets/javascripts/discourse/routes/admin-plugins-show-discourse-ai-features-edit.js @@ -12,7 +12,7 @@ export default class AdminPluginsShowDiscourseAiFeaturesEdit extends DiscourseRo const { site_settings } = await ajax("/admin/config/site_settings.json", { data: { - filter_area: `ai-features/${currentFeature.ref}`, + filter_area: `ai-features/${currentFeature.module_name}`, plugin: "discourse-ai", category: "discourse_ai", }, diff --git a/admin/assets/javascripts/discourse/templates/admin-plugins/show/discourse-ai-features/index.gjs b/admin/assets/javascripts/discourse/templates/admin-plugins/show/discourse-ai-features/index.gjs deleted file mode 100644 index 9bf9fc9c..00000000 --- a/admin/assets/javascripts/discourse/templates/admin-plugins/show/discourse-ai-features/index.gjs +++ /dev/null @@ -1,156 +0,0 @@ -import Component from "@glimmer/component"; -import { service } from "@ember/service"; -import RouteTemplate from "ember-route-template"; -import { gt } from "truth-helpers"; -import DBreadcrumbsItem from "discourse/components/d-breadcrumbs-item"; -import DButton from "discourse/components/d-button"; -import DPageSubheader from "discourse/components/d-page-subheader"; -import { i18n } from "discourse-i18n"; - -export default RouteTemplate( - class extends Component { - @service adminPluginNavManager; - - get tableHeaders() { - const prefix = "discourse_ai.features.list.header"; - return [ - i18n(`${prefix}.name`), - i18n(`${prefix}.persona`), - i18n(`${prefix}.groups`), - "", - ]; - } - - get configuredFeatures() { - return this.args.model.filter( - (feature) => feature.enable_setting.value === true - ); - } - - get unconfiguredFeatures() { - return this.args.model.filter( - (feature) => feature.enable_setting.value === false - ); - } - - - } -); diff --git a/admin/assets/javascripts/discourse/templates/admin-plugins/show/discourse-ai-features/index.hbs b/admin/assets/javascripts/discourse/templates/admin-plugins/show/discourse-ai-features/index.hbs new file mode 100644 index 00000000..d0193ebe --- /dev/null +++ b/admin/assets/javascripts/discourse/templates/admin-plugins/show/discourse-ai-features/index.hbs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/controllers/discourse_ai/admin/ai_features_controller.rb b/app/controllers/discourse_ai/admin/ai_features_controller.rb index ad258c6c..7f087cf1 100644 --- a/app/controllers/discourse_ai/admin/ai_features_controller.rb +++ b/app/controllers/discourse_ai/admin/ai_features_controller.rb @@ -11,19 +11,22 @@ module DiscourseAi def edit raise Discourse::InvalidParameters.new(:id) if params[:id].blank? - render json: serialize_feature(DiscourseAi::Features.find_feature_by_id(params[:id].to_i)) + render json: serialize_module(DiscourseAi::Features.find_module_by_id(params[:id].to_i)) end private - def serialize_features(features) - features.map { |feature| feature.merge(persona: serialize_persona(feature[:persona])) } + def serialize_features(modules) + modules.map { |a_module| serialize_module(a_module) } end - def serialize_feature(feature) - return nil if feature.blank? + def serialize_module(a_module) + return nil if a_module.blank? - feature.merge(persona: serialize_persona(feature[:persona])) + a_module.merge( + features: + a_module[:features].map { |f| f.merge(persona: serialize_persona(f[:persona])) }, + ) end def serialize_persona(persona) diff --git a/assets/javascripts/discourse/admin/models/ai-feature.js b/assets/javascripts/discourse/admin/models/ai-feature.js index 85dfa7ca..43384e61 100644 --- a/assets/javascripts/discourse/admin/models/ai-feature.js +++ b/assets/javascripts/discourse/admin/models/ai-feature.js @@ -2,14 +2,6 @@ import RestModel from "discourse/models/rest"; export default class AiFeature extends RestModel { createProperties() { - return this.getProperties( - "id", - "name", - "ref", - "description", - "enable_setting", - "persona", - "persona_setting" - ); + return this.getProperties("id", "module", "global_enabled", "features"); } } diff --git a/assets/javascripts/discourse/components/ai-features-list.gjs b/assets/javascripts/discourse/components/ai-features-list.gjs new file mode 100644 index 00000000..e2feab9c --- /dev/null +++ b/assets/javascripts/discourse/components/ai-features-list.gjs @@ -0,0 +1,85 @@ +import { concat } from "@ember/helper"; +import { gt } from "truth-helpers"; +import DButton from "discourse/components/d-button"; +import { i18n } from "discourse-i18n"; + +const AiFeaturesList = ; + +export default AiFeaturesList; diff --git a/assets/javascripts/discourse/components/ai-features.gjs b/assets/javascripts/discourse/components/ai-features.gjs new file mode 100644 index 00000000..150e505c --- /dev/null +++ b/assets/javascripts/discourse/components/ai-features.gjs @@ -0,0 +1,90 @@ +import Component from "@glimmer/component"; +import { tracked } from "@glimmer/tracking"; +import { fn } from "@ember/helper"; +import { action } from "@ember/object"; +import { service } from "@ember/service"; +import { eq } from "truth-helpers"; +import DBreadcrumbsItem from "discourse/components/d-breadcrumbs-item"; +import DButton from "discourse/components/d-button"; +import DPageSubheader from "discourse/components/d-page-subheader"; +import concatClass from "discourse/helpers/concat-class"; +import { i18n } from "discourse-i18n"; +import AiFeaturesList from "./ai-features-list"; + +const CONFIGURED = "configured"; +const UNCONFIGURED = "unconfigured"; + +export default class AiFeatures extends Component { + @service adminPluginNavManager; + + @tracked selectedFeatureGroup = CONFIGURED; + + constructor() { + super(...arguments); + + if (this.configuredFeatures.length === 0) { + this.selectedFeatureGroup = UNCONFIGURED; + } + } + + get featureGroups() { + return [ + { id: CONFIGURED, label: "discourse_ai.features.nav.configured" }, + { id: UNCONFIGURED, label: "discourse_ai.features.nav.unconfigured" }, + ]; + } + + get configuredFeatures() { + return this.args.features.filter( + (feature) => feature.module_enabled === true + ); + } + + get unconfiguredFeatures() { + return this.args.features.filter( + (feature) => feature.module_enabled === false + ); + } + + @action + selectFeatureGroup(groupId) { + this.selectedFeatureGroup = groupId; + } + + +} diff --git a/assets/javascripts/initializers/admin-plugin-configuration-nav.js b/assets/javascripts/initializers/admin-plugin-configuration-nav.js index 391f5cbc..d9ca17a0 100644 --- a/assets/javascripts/initializers/admin-plugin-configuration-nav.js +++ b/assets/javascripts/initializers/admin-plugin-configuration-nav.js @@ -41,12 +41,11 @@ export default { route: "adminPlugins.show.discourse-ai-spam", description: "discourse_ai.spam.spam_description", }, - // TODO(@keegan / @roman): Uncomment this when structured output is merged - // { - // label: "discourse_ai.features.short_title", - // route: "adminPlugins.show.discourse-ai-features", - // description: "discourse_ai.features.description", - // }, + { + label: "discourse_ai.features.short_title", + route: "adminPlugins.show.discourse-ai-features", + description: "discourse_ai.features.description", + }, ]); }); }, diff --git a/assets/stylesheets/common/ai-features.scss b/assets/stylesheets/common/ai-features.scss index 6a29541c..524fffde 100644 --- a/assets/stylesheets/common/ai-features.scss +++ b/assets/stylesheets/common/ai-features.scss @@ -1,28 +1,51 @@ -.ai-feature-list { - &__configured-features { - margin-block: 2rem; +.ai-features-list { + margin-block: 2rem; +} + +.ai-module { + &__header { + border-bottom: 1px solid var(--primary-low); + padding-bottom: 0.5rem; } - &__row-item-name, - &__row-item-description { - display: block; + &__module-title { + display: flex; + justify-content: space-between; + } +} + +.ai-feature-cards { + margin-top: 0.5rem; +} + +.ai-feature-card { + background: var(--primary-very-low); + border: 1px solid var(--primary-low); + padding: 0.5rem; + display: block; + + &__persona, + &__groups { + font-size: var(--font-down-1-rem); } - &__row-item-persona { - padding: 0; - text-align: left; - + &__persona { @include ellipsis; } - &__row-item-groups { + &__persona-button { + padding-left: 0; + } + + &__item-groups { list-style: none; display: flex; flex-flow: row wrap; gap: 0.25em; + margin: 0.5em 0; li { - font-size: var(--font-down-2); + font-size: var(--font-down-1); border-radius: var(--d-border-radius); background: var(--primary-very-low); border: 1px solid var(--primary-low); diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 88214c5f..a7362b57 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -169,15 +169,48 @@ en: short_title: "Features" 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" - list: - header: - name: "Name" - persona: "Persona" - groups: "Groups" - edit: "Edit" - set_up: "Set up" - configured_features: "Configured features" - unconfigured_features: "Unconfigured features" + disabled: "(disabled)" + persona: "Persona:" + groups: "Groups:" + no_persona: "Not set" + no_groups: "None" + edit: "Edit" + nav: + configured: "Configured" + unconfigured: "Unconfigured" + summarization: + name: "Summaries" + description: "Makes a summarization button available that allows visitors to summarize topics" + topic_summaries: "Topic summaries" + gists: "Topic list's short summaries" + search: + name: "Search" + description: "Enhances search experience by providing AI-generated answers to queries" + discoveries: "Discoveries" + discord: + name: "Discord integration" + description: "Adds the ability to search Discord channels" + search: "Discord search" + inference: + name: "Inferred concepts" + description: "Classifies topics and posts into areas of interest / labels." + generate_concepts: "Concepts inference" + match_concepts: "Concepts matching" + deduplicate_concepts: "Concepts deduplication" + + ai_helper: + name: "Helper" + description: "Assists users in community interaction, such as creating topics, writing posts, and reading content." + proofread: Proofread text + title_suggestions: "Suggest titles" + explain: "Explain" + illustrate_post: "Illustrate post" + smart_dates: "Smart dates" + translate: "Translate" + markdown_tables: "Generate Markdown table" + custom_prompt: "Custom prompt" + image_caption: "Caption images" + modals: select_option: "Select an option..." diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index e55f12f8..2ac8c9ef 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -579,23 +579,6 @@ en: missing_provider_param: "%{param} can't be blank" bedrock_invalid_url: "Please complete all the fields to use this model." - features: - summarization: - name: "Summaries" - description: "Makes a summarization button available that allows visitors to summarize topics" - gists: - name: "Short Summaries" - description: "Adds the ability to view short summaries of topics on the topic list" - discoveries: - name: "Discobot Discoveries" - description: "Enhances search experience by providing AI-generated answers to queries" - discord_search: - name: "Discord Search" - description: "Adds the ability to search Discord channels" - inferred_concepts: - name: "Inferred Concepts" - description: "Classifies topics and posts into areas of interest / labels." - errors: quota_exceeded: "You have exceeded the quota for this model. Please try again in %{relative_time}." quota_required: "You must specify maximum tokens or usages for this model" diff --git a/config/settings.yml b/config/settings.yml index ab3675df..1bc4c5a0 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -90,15 +90,18 @@ discourse_ai: default: false client: true validator: "DiscourseAi::Configuration::LlmDependencyValidator" + area: "ai-features/ai_helper" composer_ai_helper_allowed_groups: type: group_list list_type: compact default: "3|14" # 3: @staff, 14: @trust_level_4 allow_any: false refresh: true + area: "ai-features/ai_helper" ai_helper_allowed_in_pm: default: false client: true + area: "ai-features/ai_helper" ai_helper_model: default: "" allow_any: false @@ -118,10 +121,13 @@ discourse_ai: default: "3|14" # 3: @staff, 14: @trust_level_4 allow_any: false refresh: true + area: "ai-features/ai_helper" ai_helper_automatic_chat_thread_title: default: false + area: "ai-features/ai_helper" ai_helper_automatic_chat_thread_title_delay: default: 5 + area: "ai-features/ai_helper" ai_helper_illustrate_post_model: default: disabled type: enum @@ -129,6 +135,7 @@ discourse_ai: - stable_diffusion_xl - dall_e_3 - disabled + area: "ai-features/ai_helper" ai_helper_enabled_features: client: true default: "suggestions|context_menu" @@ -140,6 +147,7 @@ discourse_ai: - "suggestions" - "context_menu" - "image_caption" + area: "ai-features/ai_helper" ai_helper_image_caption_model: default: "" type: enum @@ -152,6 +160,7 @@ discourse_ai: default: "10" # 10: @trust_level_0 allow_any: false refresh: true + area: "ai-features/ai_helper" ai_helper_model_allowed_seeded_models: default: "" hidden: true @@ -166,38 +175,47 @@ discourse_ai: default: "-22" type: enum enum: "DiscourseAi::Configuration::PersonaEnumerator" + area: "ai-features/ai_helper" ai_helper_title_suggestions_persona: default: "-23" type: enum enum: "DiscourseAi::Configuration::PersonaEnumerator" + area: "ai-features/ai_helper" ai_helper_explain_persona: default: "-24" type: enum enum: "DiscourseAi::Configuration::PersonaEnumerator" + area: "ai-features/ai_helper" ai_helper_post_illustrator_persona: default: "-21" type: enum enum: "DiscourseAi::Configuration::PersonaEnumerator" + area: "ai-features/ai_helper" ai_helper_smart_dates_persona: default: "-19" type: enum enum: "DiscourseAi::Configuration::PersonaEnumerator" + area: "ai-features/ai_helper" ai_helper_translator_persona: default: "-25" type: enum enum: "DiscourseAi::Configuration::PersonaEnumerator" + area: "ai-features/ai_helper" ai_helper_markdown_tables_persona: default: "-20" type: enum enum: "DiscourseAi::Configuration::PersonaEnumerator" + area: "ai-features/ai_helper" ai_helper_custom_prompt_persona: default: "-18" type: enum enum: "DiscourseAi::Configuration::PersonaEnumerator" + area: "ai-features/ai_helper" ai_helper_image_caption_persona: default: "-26" type: enum enum: "DiscourseAi::Configuration::PersonaEnumerator" + area: "ai-features/ai_helper" ai_embeddings_enabled: default: false @@ -298,12 +316,12 @@ discourse_ai: hidden: true ai_summary_gists_enabled: default: false - area: "ai-features/gists" + area: "ai-features/summarization" ai_summary_gists_persona: default: "-12" type: enum enum: "DiscourseAi::Configuration::PersonaEnumerator" - area: "ai-features/gists" + area: "ai-features/summarization" ai_summary_gists_allowed_groups: # Deprecated. TODO(roman): Remove 2025-09-01 type: group_list list_type: compact @@ -331,7 +349,7 @@ discourse_ai: ai_bot_enabled: default: false client: true - area: "ai-features/discoveries" + area: "ai-features/search" ai_bot_enable_chat_warning: default: false client: true @@ -367,7 +385,7 @@ discourse_ai: type: enum client: true enum: "DiscourseAi::Configuration::PersonaEnumerator" - area: "ai-features/discoveries" + area: "ai-features/search" ai_automation_max_triage_per_minute: default: 60 hidden: true @@ -383,32 +401,32 @@ discourse_ai: ai_discord_search_enabled: default: false client: true - area: "ai-features/discord_search" + area: "ai-features/discord" ai_discord_app_id: default: "" client: false - area: "ai-features/discord_search" + area: "ai-features/discord" ai_discord_app_public_key: default: "" client: false - area: "ai-features/discord_search" + area: "ai-features/discord" ai_discord_search_mode: default: "search" type: enum choices: - search - persona - area: "ai-features/discord_search" + area: "ai-features/discord" ai_discord_search_persona: default: "" type: enum enum: "DiscourseAi::Configuration::PersonaEnumerator" - area: "ai-features/discord_search" + area: "ai-features/discord" ai_discord_allowed_guilds: type: list list_type: compact default: "" - area: "ai-features/discord_search" + area: "ai-features/discord" ai_spam_detection_enabled: default: false @@ -459,51 +477,51 @@ discourse_ai: inferred_concepts_enabled: default: false client: true - area: "ai-features/inferred_concepts" + area: "ai-features/inference" inferred_concepts_background_match: default: false client: false - area: "ai-features/inferred_concepts" + area: "ai-features/inference" inferred_concepts_daily_topics_limit: default: 20 client: false - area: "ai-features/inferred_concepts" + area: "ai-features/inference" inferred_concepts_min_posts: default: 5 client: false - area: "ai-features/inferred_concepts" + area: "ai-features/inference" inferred_concepts_min_likes: default: 10 client: false - area: "ai-features/inferred_concepts" + area: "ai-features/inference" inferred_concepts_min_views: default: 100 client: false - area: "ai-features/inferred_concepts" + area: "ai-features/inference" inferred_concepts_lookback_days: default: 30 client: false - area: "ai-features/inferred_concepts" + area: "ai-features/inference" inferred_concepts_daily_posts_limit: default: 30 client: false - area: "ai-features/inferred_concepts" + area: "ai-features/inference" inferred_concepts_post_min_likes: default: 5 client: false - area: "ai-features/inferred_concepts" + area: "ai-features/inference" inferred_concepts_generate_persona: default: "-15" type: enum enum: "DiscourseAi::Configuration::PersonaEnumerator" - area: "ai-features/inferred_concepts" + area: "ai-features/inference" inferred_concepts_match_persona: default: "-16" type: enum enum: "DiscourseAi::Configuration::PersonaEnumerator" - area: "ai-features/inferred_concepts" + area: "ai-features/inference" inferred_concepts_deduplicate_persona: default: "-17" type: enum enum: "DiscourseAi::Configuration::PersonaEnumerator" - area: "ai-features/inferred_concepts" + area: "ai-features/inference" diff --git a/lib/features.rb b/lib/features.rb index 41d6d832..a1070d2d 100644 --- a/lib/features.rb +++ b/lib/features.rb @@ -2,91 +2,107 @@ module DiscourseAi module Features - def self.feature_config + def self.features_config [ { id: 1, - name_ref: "summarization", - name_key: "discourse_ai.features.summarization.name", - description_key: "discourse_ai.features.summarization.description", - persona_setting_name: "ai_summarization_persona", - enable_setting_name: "ai_summarization_enabled", + module_name: "summarization", + module_enabled: "ai_summarization_enabled", + features: [ + { name: "topic_summaries", persona_setting_name: "ai_summarization_persona" }, + { + name: "gists", + persona_setting_name: "ai_summary_gists_persona", + enabled: "ai_summary_gists_enabled", + }, + ], }, { id: 2, - name_ref: "gists", - name_key: "discourse_ai.features.gists.name", - description_key: "discourse_ai.features.gists.description", - persona_setting_name: "ai_summary_gists_persona", - enable_setting_name: "ai_summary_gists_enabled", + module_name: "search", + module_enabled: "ai_bot_enabled", + features: [{ name: "discoveries", persona_setting_name: "ai_bot_discover_persona" }], }, { id: 3, - name_ref: "discoveries", - name_key: "discourse_ai.features.discoveries.name", - description_key: "discourse_ai.features.discoveries.description", - persona_setting_name: "ai_bot_discover_persona", - enable_setting_name: "ai_bot_enabled", + module_name: "discord", + module_enabled: "ai_discord_search_enabled", + features: [{ name: "search", persona_setting_name: "ai_discord_search_persona" }], }, { id: 4, - name_ref: "discord_search", - name_key: "discourse_ai.features.discord_search.name", - description_key: "discourse_ai.features.discord_search.description", - persona_setting_name: "ai_discord_search_persona", - enable_setting_name: "ai_discord_search_enabled", + module_name: "inference", + module_enabled: "inferred_concepts_enabled", + features: [ + { + name: "generate_concepts", + persona_setting_name: "inferred_concepts_generate_persona", + }, + { name: "match_concepts", persona_setting_name: "inferred_concepts_match_persona" }, + { + name: "deduplicate_concepts", + persona_setting_name: "inferred_concepts_deduplicate_persona", + }, + ], }, { id: 5, - name_ref: "inferred_concepts", - name_key: "discourse_ai.features.inferred_concepts.name", - description_key: "discourse_ai.features.inferred_concepts.description", - persona_setting_name: "inferred_concepts_generate_persona", - enable_setting_name: "inferred_concepts_enabled", + module_name: "ai_helper", + module_enabled: "ai_helper_enabled", + features: [ + { name: "proofread", persona_setting_name: "ai_helper_proofreader_persona" }, + { + name: "title_suggestions", + persona_setting_name: "ai_helper_title_suggestions_persona", + }, + { name: "explain", persona_setting_name: "ai_helper_explain_persona" }, + { name: "illustrate_post", persona_setting_name: "ai_helper_post_illustrator_persona" }, + { name: "smart_dates", persona_setting_name: "ai_helper_smart_dates_persona" }, + { name: "translate", persona_setting_name: "ai_helper_translator_persona" }, + { name: "markdown_tables", persona_setting_name: "ai_helper_markdown_tables_persona" }, + { name: "custom_prompt", persona_setting_name: "ai_helper_custom_prompt_persona" }, + { name: "image_caption", persona_setting_name: "ai_helper_image_caption_persona" }, + ], }, ] end def self.features - feature_config.map do |feature| + features_config.map do |a_module| { - id: feature[:id], - ref: feature[:name_ref], - name: I18n.t(feature[:name_key]), - description: I18n.t(feature[:description_key]), - persona: AiPersona.find_by(id: SiteSetting.get(feature[:persona_setting_name])), - persona_setting: { - name: feature[:persona_setting_name], - value: SiteSetting.get(feature[:persona_setting_name]), - type: SiteSetting.type_supervisor.get_type(feature[:persona_setting_name]), - }, - enable_setting: { - name: feature[:enable_setting_name], - value: SiteSetting.get(feature[:enable_setting_name]), - type: SiteSetting.type_supervisor.get_type(feature[:enable_setting_name]), - }, + id: a_module[:id], + module_name: a_module[:module_name], + module_enabled: SiteSetting.get(a_module[:module_enabled]), + features: + a_module[:features].map do |feature| + { + name: feature[:name], + persona: AiPersona.find_by(id: SiteSetting.get(feature[:persona_setting_name])), + enabled: feature[:enabled].present? ? SiteSetting.get(feature[:enabled]) : true, + } + end, } end end - def self.find_feature_by_id(id) + def self.find_module_by_id(id) lookup = features.index_by { |f| f[:id] } lookup[id] end - def self.find_feature_by_ref(name_ref) - lookup = features.index_by { |f| f[:ref] } - lookup[name_ref] + def self.find_module_by_name(module_name) + lookup = features.index_by { |f| f[:module] } + lookup[module_name] end - def self.find_feature_id_by_ref(name_ref) - find_feature_by_ref(name_ref)&.dig(:id) + def self.find_module_id_by_name(module_name) + find_module_by_name(module_name)&.dig(:id) end - def self.feature_area(name_ref) - name_ref = name_ref.to_s if name_ref.is_a?(Symbol) - find_feature_by_ref(name_ref) || raise(ArgumentError, "Feature not found: #{name_ref}") - "ai-features/#{name_ref}" + def self.feature_area(module_name) + name_s = module_name.to_s + find_module_by_name(name_s) || raise(ArgumentError, "Feature not found: #{name_s}") + "ai-features/#{name_s}" end end end diff --git a/plugin.rb b/plugin.rb index 7927c2da..bda721fa 100644 --- a/plugin.rb +++ b/plugin.rb @@ -74,8 +74,8 @@ Rails.autoloaders.main.push_dir(File.join(__dir__, "lib"), namespace: ::Discours require_relative "lib/engine" require_relative "lib/features" -DiscourseAi::Features.feature_config.each do |feature| - register_site_setting_area("ai-features/#{feature[:name_ref]}") +DiscourseAi::Features.features_config.each do |feature| + register_site_setting_area("ai-features/#{feature[:module_name]}") end after_initialize do diff --git a/spec/requests/admin/ai_features_controller_spec.rb b/spec/requests/admin/ai_features_controller_spec.rb index 45b6c7d5..2dcda73c 100644 --- a/spec/requests/admin/ai_features_controller_spec.rb +++ b/spec/requests/admin/ai_features_controller_spec.rb @@ -26,7 +26,7 @@ RSpec.describe DiscourseAi::Admin::AiFeaturesController do describe "#edit" do it "returns a success response" do get "/admin/plugins/discourse-ai/ai-features/1/edit.json" - expect(response.parsed_body["name"]).to eq(I18n.t "discourse_ai.features.summarization.name") + expect(response.parsed_body["module_name"]).to eq("summarization") end end end diff --git a/spec/system/admin_ai_features_spec.rb b/spec/system/admin_ai_features_spec.rb index fa451c69..5b79232d 100644 --- a/spec/system/admin_ai_features_spec.rb +++ b/spec/system/admin_ai_features_spec.rb @@ -21,43 +21,37 @@ RSpec.describe "Admin AI features configuration", type: :system, js: true do it "lists all persona backed AI features separated by configured/unconfigured" do ai_features_page.visit - expect( - ai_features_page - .configured_features_table - .find(".ai-feature-list__row-item .ai-feature-list__row-item-name") - .text, - ).to eq(I18n.t("discourse_ai.features.summarization.name")) + ai_features_page.toggle_configured - expect(ai_features_page).to have_configured_feature_items(1) - expect(ai_features_page).to have_unconfigured_feature_items(4) + expect(ai_features_page).to have_listed_modules(1) + + ai_features_page.toggle_unconfigured + + expect(ai_features_page).to have_listed_modules(4) end it "lists the persona used for the corresponding AI feature" do ai_features_page.visit - expect(ai_features_page).to have_feature_persona(summarization_persona.name) + + ai_features_page.toggle_configured + + expect(ai_features_page).to have_feature_persona("topic_summaries", summarization_persona.name) end it "lists the groups allowed to use the AI feature" do ai_features_page.visit - expect(ai_features_page).to have_feature_groups([group_1.name, group_2.name]) - end - it "can navigate the AI plugin with breadcrumbs" do - visit "/admin/plugins/discourse-ai/ai-features" - expect(page).to have_css(".d-breadcrumbs") - expect(page).to have_css(".d-breadcrumbs__item", count: 4) - find(".d-breadcrumbs__item", text: I18n.t("admin_js.admin.plugins.title")).click - expect(page).to have_current_path("/admin/plugins") + ai_features_page.toggle_configured + + expect(ai_features_page).to have_feature_groups("topic_summaries", [group_1.name, group_2.name]) end it "shows edit page with settings" do ai_features_page.visit - ai_features_page.click_edit_feature(I18n.t("discourse_ai.features.summarization.name")) + + ai_features_page.click_edit_module("summarization") + expect(page).to have_current_path("/admin/plugins/discourse-ai/ai-features/1/edit") - expect(page).to have_css( - ".ai-feature-editor__header h2", - text: I18n.t("discourse_ai.features.summarization.name"), - ) expect(page).to have_css(".setting") end diff --git a/spec/system/page_objects/pages/admin_ai_features.rb b/spec/system/page_objects/pages/admin_ai_features.rb index e6ad9efd..c632cef5 100644 --- a/spec/system/page_objects/pages/admin_ai_features.rb +++ b/spec/system/page_objects/pages/admin_ai_features.rb @@ -3,48 +3,44 @@ module PageObjects module Pages class AdminAiFeatures < PageObjects::Pages::Base - CONFIGURED_FEATURES_TABLE = ".ai-feature-list__configured-features .d-admin-table" - UNCONFIGURED_FEATURES_TABLE = ".ai-feature-list__unconfigured-features .d-admin-table" + FEATURES_PAGE = ".ai-features" def visit page.visit("/admin/plugins/discourse-ai/ai-features") self end - def configured_features_table - page.find(CONFIGURED_FEATURES_TABLE) + def toggle_configured + page.find("#{FEATURES_PAGE} .ai-feature-groups .configured").click end - def unconfigured_features_table - page.find(UNCONFIGURED_FEATURES_TABLE) + def toggle_unconfigured + page.find("#{FEATURES_PAGE} .ai-feature-groups .unconfigured").click end - def has_configured_feature_items?(count) - page.has_css?("#{CONFIGURED_FEATURES_TABLE} .ai-feature-list__row", count: count) + def has_listed_modules?(count) + page.has_css?("#{FEATURES_PAGE} .ai-module", count: count) end - def has_unconfigured_feature_items?(count) - page.has_css?("#{UNCONFIGURED_FEATURES_TABLE} .ai-feature-list__row", count: count) - end - - def has_feature_persona?(name) + def has_feature_persona?(feature_name, name) page.has_css?( - "#{CONFIGURED_FEATURES_TABLE} .ai-feature-list__persona .d-button-label ", + "#{FEATURES_PAGE} .ai-feature-card[data-feature-name='#{feature_name}'] .ai-feature-card__persona-button .d-button-label", text: name, ) end - def has_feature_groups?(groups) - listed_groups = page.find("#{CONFIGURED_FEATURES_TABLE} .ai-feature-list__groups") + def has_feature_groups?(feature_name, groups) + listed_groups = + page.find( + "#{FEATURES_PAGE} .ai-feature-card[data-feature-name='#{feature_name}'] .ai-feature-card__item-groups", + ) list_items = listed_groups.all("li", visible: true).map(&:text) list_items.sort == groups.sort end - def click_edit_feature(feature_name) - page.find( - "#{CONFIGURED_FEATURES_TABLE} .ai-feature-list__row[data-feature-name='#{feature_name}'] .edit", - ).click + def click_edit_module(module_name) + page.find("#{FEATURES_PAGE} .ai-module[data-module-name='#{module_name}'] .edit").click end end end