Skip to content
This repository was archived by the owner on Jul 22, 2025. It is now read-only.
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import DiscourseRoute from "discourse/routes/discourse";

export default class AdminPluginsShowDiscourseAiFeaturesEdit extends DiscourseRoute {
async model(params) {
const allFeatures = this.modelFor(
"adminPlugins.show.discourse-ai-features"
);
const id = parseInt(params.id, 10);

return allFeatures.find((feature) => feature.id === id);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { service } from "@ember/service";
import DiscourseRoute from "discourse/routes/discourse";

export default class AdminPluginsShowDiscourseAiFeatures extends DiscourseRoute {
@service store;

async model() {
return this.store.findAll("ai-feature");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import RouteTemplate from "ember-route-template";
import AiFeatureEditor from "discourse/plugins/discourse-ai/discourse/components/ai-feature-editor";

export default RouteTemplate(
<template>
<AiFeatureEditor @model={{@model}} />
</template>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
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;
@service currentUser;

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
);
}

<template>
<DBreadcrumbsItem
@path="/admin/plugins/{{this.adminPluginNavManager.currentPlugin.name}}/ai-features"
@label={{i18n "discourse_ai.features.short_title"}}
/>
<section class="ai-feature-list admin-detail">
<DPageSubheader
@titleLabel={{i18n "discourse_ai.features.short_title"}}
@descriptionLabel={{i18n "discourse_ai.features.description"}}
@learnMoreUrl="todo"
/>

{{#if (gt this.configuredFeatures.length 0)}}
<div class="ai-feature-list__configured-features">
<h3>{{i18n "discourse_ai.features.list.configured_features"}}</h3>

<table class="d-admin-table">
<thead>
<tr>
{{#each this.tableHeaders as |header|}}
<th>{{header}}</th>
{{/each}}
</tr>
</thead>

<tbody>
{{#each this.configuredFeatures as |feature|}}
<tr class="ai-feature-list__row d-admin-row__content">
<td class="d-admin-row__overview ai-feature-list__row-item">
<span class="ai-feature-list__row-item-name">
<strong>
{{feature.name}}
</strong>
</span>
<span class="ai-feature-list__row-item-description">
{{feature.description}}
</span>
</td>
<td class="d-admin-row__detail ai-feature-list__row-item">
<DButton
class="btn-flat btn-small ai-feature-list__row-item-persona"
@translatedLabel={{feature.persona.name}}
@route="adminPlugins.show.discourse-ai-personas.edit"
@routeModels={{feature.persona.id}}
/>
</td>
<td class="d-admin-row__detail ai-feature-list__row-item">
{{#if (gt feature.persona.allowed_groups.length 0)}}
<ul class="ai-feature-list__row-item-groups">
{{#each feature.persona.allowed_groups as |group|}}
<li>{{group.name}}</li>
{{/each}}
</ul>
{{/if}}
</td>
<td class="d-admin-row_controls">
<DButton
class="btn-small"
@translatedLabel="Edit"
@route="adminPlugins.show.discourse-ai-features.edit"
@routeModels={{feature.id}}
/>
</td>
</tr>
{{/each}}
</tbody>
</table>
</div>
{{/if}}

{{#if (gt this.unconfiguredFeatures.length 0)}}
<div class="ai-feature-list-editor__unconfigured-features">
<h3>{{i18n "discourse_ai.features.list.unconfigured_features"}}</h3>

<table class="d-admin-table">
<thead>
<tr>
<th>{{i18n "discourse_ai.features.list.header.name"}}</th>
<th></th>
</tr>
</thead>

<tbody>
{{#each this.unconfiguredFeatures as |feature|}}
<tr class="ai-feature-list__row d-admin-row__content">
<td class="d-admin-row__overview ai-feature-list__row-item">
<span class="ai-feature-list__row-item-name">
<strong>
{{feature.name}}
</strong>
</span>
<span class="ai-feature-list__row-item-description">
{{feature.description}}
</span>
</td>

<td class="d-admin-row_controls">
<DButton
class="btn-small"
@label="discourse_ai.features.list.set_up"
@route="adminPlugins.show.discourse-ai-features.edit"
@routeModels={{feature.id}}
/>
</td>
</tr>
{{/each}}
</tbody>
</table>
</div>
{{/if}}
</section>
</template>
}
);
105 changes: 105 additions & 0 deletions app/controllers/discourse_ai/admin/ai_features_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# frozen_string_literal: true

module DiscourseAi
module Admin
class AiFeaturesController < ::Admin::AdminController
requires_plugin ::DiscourseAi::PLUGIN_NAME

def index
render json: persona_backed_features
end

def edit
raise Discourse::InvalidParameters.new(:id) if params[:id].blank?
render json: find_feature_by_id(params[:id].to_i)
end

def update
raise Discourse::InvalidParameters.new(:id) if params[:id].blank?
raise Discourse::InvalidParameters.new(:ai_feature) if params[:ai_feature].blank?
if params[:ai_feature][:persona_id].blank?
raise Discourse::InvalidParameters.new(:persona_id)
end
raise Discourse::InvalidParameters.new(:enabled) if params[:ai_feature][:enabled].nil?

feature = find_feature_by_id(params[:id].to_i)
enable_value = params[:ai_feature][:enabled]
persona_id = params[:ai_feature][:persona_id]

SiteSetting.set_and_log(feature[:enable_setting][:name], enable_value, guardian.user)
SiteSetting.set_and_log(feature[:persona_setting][:name], persona_id, guardian.user)

render json: find_feature_by_id(params[:id].to_i)
end

private

# Eventually we may move this all to an active record model
# but for now we are just using a hash
# to store the features and their corresponding settings
def feature_config
[
{
id: 1,
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",
},
{
id: 2,
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",
},
{
id: 3,
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",
},
{
id: 4,
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",
},
]
end

def persona_backed_features
feature_config.map do |feature|
{
id: feature[:id],
name: I18n.t(feature[:name_key]),
description: I18n.t(feature[:description_key]),
persona:
serialize_data(
AiPersona.find_by(id: SiteSetting.get(feature[:persona_setting_name])),
AiFeaturesPersonaSerializer,
root: false,
),
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]),
},
}
end
end

def find_feature_by_id(id)
lookup = persona_backed_features.index_by { |feature| feature[:id] }
lookup[id]
end
end
end
end
2 changes: 2 additions & 0 deletions app/jobs/regular/stream_discord_reply.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ class StreamDiscordReply < ::Jobs::Base
def execute(args)
interaction = args[:interaction]

return unless SiteSetting.ai_discord_search_enabled

if SiteSetting.ai_discord_search_mode == "persona"
DiscourseAi::Discord::Bot::PersonaReplier.new(interaction).handle_interaction!
else
Expand Down
12 changes: 12 additions & 0 deletions app/serializers/ai_features_persona_serializer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# frozen_string_literal: true

class AiFeaturesPersonaSerializer < ApplicationSerializer
attributes :id, :name, :system_prompt, :allowed_groups, :enabled

def allowed_groups
Group
.where(id: object.allowed_group_ids)
.pluck(:id, :name)
.map { |id, name| { id: id, name: name } }
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,9 @@ export default {
this.route("edit", { path: "/:id/edit" });
}
);

this.route("discourse-ai-features", { path: "ai-features" }, function () {
this.route("edit", { path: "/:id/edit" });
});
},
};
21 changes: 21 additions & 0 deletions assets/javascripts/discourse/admin/adapters/ai-feature.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import RestAdapter from "discourse/adapters/rest";

export default class AiFeatureAdapter extends RestAdapter {
jsonMode = true;

basePath() {
return "/admin/plugins/discourse-ai/";
}

pathFor(store, type, findArgs) {
// removes underscores which are implemented in base
let path =
this.basePath(store, type, findArgs) +
store.pluralize(this.apiNameFor(type));
return this.appendQueryParams(path, findArgs);
}

apiNameFor() {
return "ai-feature";
}
}
14 changes: 14 additions & 0 deletions assets/javascripts/discourse/admin/models/ai-feature.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import RestModel from "discourse/models/rest";

export default class AiFeature extends RestModel {
createProperties() {
return this.getProperties(
"id",
"name",
"description",
"enable_setting",
"persona",
"persona_setting"
);
}
}
Loading
Loading