Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d29584c1cc |
+7
-7
@@ -1,18 +1,18 @@
|
||||
import DiscourseRoute from "discourse/routes/discourse";
|
||||
|
||||
export default class AdminPluginsShowDiscourseAiLlmsNew extends DiscourseRoute {
|
||||
queryParams = {
|
||||
export default DiscourseRoute.extend({
|
||||
queryParams: {
|
||||
llmTemplate: { refreshModel: true },
|
||||
};
|
||||
},
|
||||
|
||||
async model() {
|
||||
const record = this.store.createRecord("ai-llm");
|
||||
record.provider_params = {};
|
||||
return record;
|
||||
}
|
||||
},
|
||||
|
||||
setupController(controller, model) {
|
||||
super.setupController(controller, model);
|
||||
this._super(controller, model);
|
||||
controller.set(
|
||||
"allLlms",
|
||||
this.modelFor("adminPlugins.show.discourse-ai-llms")
|
||||
@@ -21,5 +21,5 @@ export default class AdminPluginsShowDiscourseAiLlmsNew extends DiscourseRoute {
|
||||
"llmTemplate",
|
||||
this.paramsFor(this.routeName).llmTemplate || null
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
+5
-5
@@ -1,19 +1,19 @@
|
||||
import DiscourseRoute from "discourse/routes/discourse";
|
||||
|
||||
export default class AdminPluginsShowDiscourseAiLlmsShow extends DiscourseRoute {
|
||||
export default DiscourseRoute.extend({
|
||||
async model(params) {
|
||||
const allLlms = this.modelFor("adminPlugins.show.discourse-ai-llms");
|
||||
const id = parseInt(params.id, 10);
|
||||
const record = allLlms.findBy("id", id);
|
||||
record.provider_params = record.provider_params || {};
|
||||
return record;
|
||||
}
|
||||
},
|
||||
|
||||
setupController(controller, model) {
|
||||
super.setupController(controller, model);
|
||||
this._super(controller, model);
|
||||
controller.set(
|
||||
"allLlms",
|
||||
this.modelFor("adminPlugins.show.discourse-ai-llms")
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
+5
-5
@@ -1,7 +1,7 @@
|
||||
import { AUTO_GROUPS } from "discourse/lib/constants";
|
||||
import DiscourseRoute from "discourse/routes/discourse";
|
||||
|
||||
export default class AdminPluginsShowDiscourseAiPersonasNew extends DiscourseRoute {
|
||||
export default DiscourseRoute.extend({
|
||||
async model() {
|
||||
const record = this.store.createRecord("ai-persona");
|
||||
record.set("allowed_group_ids", [AUTO_GROUPS.trust_level_0.id]);
|
||||
@@ -13,13 +13,13 @@ export default class AdminPluginsShowDiscourseAiPersonasNew extends DiscourseRou
|
||||
record.set("allow_personal_messages", true);
|
||||
record.set("tool_details", false);
|
||||
return record;
|
||||
}
|
||||
},
|
||||
|
||||
setupController(controller, model) {
|
||||
super.setupController(controller, model);
|
||||
this._super(controller, model);
|
||||
controller.set(
|
||||
"allPersonas",
|
||||
this.modelFor("adminPlugins.show.discourse-ai-personas")
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
+5
-5
@@ -1,19 +1,19 @@
|
||||
import DiscourseRoute from "discourse/routes/discourse";
|
||||
|
||||
export default class AdminPluginsShowDiscourseAiPersonasShow extends DiscourseRoute {
|
||||
export default DiscourseRoute.extend({
|
||||
async model(params) {
|
||||
const allPersonas = this.modelFor(
|
||||
"adminPlugins.show.discourse-ai-personas"
|
||||
);
|
||||
const id = parseInt(params.id, 10);
|
||||
return allPersonas.findBy("id", id);
|
||||
}
|
||||
},
|
||||
|
||||
setupController(controller, model) {
|
||||
super.setupController(controller, model);
|
||||
this._super(controller, model);
|
||||
controller.set(
|
||||
"allPersonas",
|
||||
this.modelFor("adminPlugins.show.discourse-ai-personas")
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { service } from "@ember/service";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import DiscourseRoute from "discourse/routes/discourse";
|
||||
|
||||
export default class DiscourseAiUsageRoute extends DiscourseRoute {
|
||||
@service store;
|
||||
|
||||
model() {
|
||||
return ajax("/admin/plugins/discourse-ai/ai-usage.json");
|
||||
}
|
||||
}
|
||||
+5
-7
@@ -1,7 +1,5 @@
|
||||
<section class="ai-persona-tool-editor__current admin-detail pull-left">
|
||||
<AiToolEditor
|
||||
@tools={{this.allTools}}
|
||||
@model={{this.model}}
|
||||
@presets={{this.presets}}
|
||||
/>
|
||||
</section>
|
||||
<AiToolEditor
|
||||
@tools={{this.allTools}}
|
||||
@model={{this.model}}
|
||||
@presets={{this.presets}}
|
||||
/>
|
||||
+5
-7
@@ -1,7 +1,5 @@
|
||||
<section class="ai-persona-tool-editor__current admin-detail pull-left">
|
||||
<AiToolEditor
|
||||
@tools={{this.allTools}}
|
||||
@model={{this.model}}
|
||||
@presets={{this.presets}}
|
||||
/>
|
||||
</section>
|
||||
<AiToolEditor
|
||||
@tools={{this.allTools}}
|
||||
@model={{this.model}}
|
||||
@presets={{this.presets}}
|
||||
/>
|
||||
@@ -1 +0,0 @@
|
||||
<AiUsage @model={{this.model}} />
|
||||
@@ -1,27 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module DiscourseAi
|
||||
module Admin
|
||||
class AiUsageController < ::Admin::AdminController
|
||||
requires_plugin "discourse-ai"
|
||||
|
||||
def show
|
||||
render json: AiUsageSerializer.new(create_report, root: false)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_report
|
||||
report =
|
||||
DiscourseAi::Completions::Report.new(
|
||||
start_date: params[:start_date]&.to_date || 30.days.ago,
|
||||
end_date: params[:end_date]&.to_date || Time.current,
|
||||
)
|
||||
|
||||
report = report.filter_by_feature(params[:feature]) if params[:feature].present?
|
||||
report = report.filter_by_model(params[:model]) if params[:model].present?
|
||||
report
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -35,7 +35,7 @@ module DiscourseAi
|
||||
prompt.custom_instruction = params[:custom_prompt]
|
||||
end
|
||||
|
||||
return suggest_thumbnails(input) if prompt.id == CompletionPrompt::ILLUSTRATE_POST
|
||||
suggest_thumbnails(input) if prompt.id == CompletionPrompt::ILLUSTRATE_POST
|
||||
|
||||
hijack do
|
||||
render json:
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module ::Jobs
|
||||
class HotTopicsGistBatch < ::Jobs::Base
|
||||
def execute(_args)
|
||||
return if !SiteSetting.discourse_ai_enabled
|
||||
return if !SiteSetting.ai_summarization_enabled
|
||||
return if SiteSetting.ai_summarize_max_hot_topics_gists_per_batch.zero?
|
||||
|
||||
Topic
|
||||
.joins("JOIN topic_hot_scores on topics.id = topic_hot_scores.topic_id")
|
||||
.order("topic_hot_scores.score DESC")
|
||||
.limit(SiteSetting.ai_summarize_max_hot_topics_gists_per_batch)
|
||||
.each do |topic|
|
||||
summarizer = DiscourseAi::Summarization.topic_gist(topic)
|
||||
gist = summarizer.existing_summary
|
||||
|
||||
summarizer.force_summarize(Discourse.system_user) if gist.blank? || gist.outdated
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -9,7 +9,9 @@ module ::Jobs
|
||||
post = Post.find_by(id: post_id, post_type: Post.types[:regular])
|
||||
return if post&.raw.blank?
|
||||
|
||||
DiscourseAi::Sentiment::PostClassification.new.classify!(post)
|
||||
DiscourseAi::PostClassificator.new(
|
||||
DiscourseAi::Sentiment::SentimentClassification.new,
|
||||
).classify!(post)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
+6
-3
@@ -1,20 +1,23 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module ::Jobs
|
||||
class FastTrackTopicGist < ::Jobs::Base
|
||||
class UpdateHotTopicGist < ::Jobs::Base
|
||||
sidekiq_options retry: false
|
||||
|
||||
def execute(args)
|
||||
return if !SiteSetting.discourse_ai_enabled
|
||||
return if !SiteSetting.ai_summarization_enabled
|
||||
return if !SiteSetting.ai_summary_gists_enabled
|
||||
return if SiteSetting.ai_summarize_max_hot_topics_gists_per_batch.zero?
|
||||
|
||||
topic = Topic.find_by(id: args[:topic_id])
|
||||
return if topic.blank?
|
||||
|
||||
return if !TopicHotScore.where(topic: topic).exists?
|
||||
|
||||
summarizer = DiscourseAi::Summarization.topic_gist(topic)
|
||||
gist = summarizer.existing_summary
|
||||
return if gist.present? && !gist.outdated
|
||||
return if gist.blank?
|
||||
return if !gist.outdated
|
||||
|
||||
summarizer.force_summarize(Discourse.system_user)
|
||||
end
|
||||
@@ -70,15 +70,14 @@ module Jobs
|
||||
Post
|
||||
.joins("LEFT JOIN #{table_name} ON #{table_name}.post_id = posts.id")
|
||||
.where(deleted_at: nil)
|
||||
.where(post_type: Post.types[:regular])
|
||||
.limit(limit - rebaked)
|
||||
|
||||
# First, we'll try to backfill embeddings for posts that have none
|
||||
posts
|
||||
.where("#{table_name}.post_id IS NULL")
|
||||
.find_in_batches do |batch|
|
||||
vector_rep.gen_bulk_reprensentations(batch)
|
||||
rebaked += batch.size
|
||||
.find_each do |t|
|
||||
vector_rep.generate_representation_from(t)
|
||||
rebaked += 1
|
||||
end
|
||||
|
||||
return if rebaked >= limit
|
||||
@@ -91,28 +90,24 @@ module Jobs
|
||||
OR
|
||||
#{table_name}.strategy_version < #{strategy.version}
|
||||
SQL
|
||||
.find_in_batches do |batch|
|
||||
vector_rep.gen_bulk_reprensentations(batch)
|
||||
rebaked += batch.size
|
||||
.find_each do |t|
|
||||
vector_rep.generate_representation_from(t)
|
||||
rebaked += 1
|
||||
end
|
||||
|
||||
return if rebaked >= limit
|
||||
|
||||
# Finally, we'll try to backfill embeddings for posts that have outdated
|
||||
# embeddings due to edits. Here we only do 10% of the limit
|
||||
posts_batch_size = 1000
|
||||
|
||||
outdated_post_ids =
|
||||
posts
|
||||
.where("#{table_name}.updated_at < ?", 7.days.ago)
|
||||
.order("random()")
|
||||
.limit((limit - rebaked) / 10)
|
||||
.pluck(:id)
|
||||
|
||||
outdated_post_ids.each_slice(posts_batch_size) do |batch|
|
||||
vector_rep.gen_bulk_reprensentations(Post.where(id: batch))
|
||||
rebaked += batch.length
|
||||
end
|
||||
posts
|
||||
.where("#{table_name}.updated_at < ?", 7.days.ago)
|
||||
.order("random()")
|
||||
.limit((limit - rebaked) / 10)
|
||||
.pluck(:id)
|
||||
.each do |id|
|
||||
vector_rep.generate_representation_from(Post.find_by(id: id))
|
||||
rebaked += 1
|
||||
end
|
||||
|
||||
rebaked
|
||||
end
|
||||
@@ -125,13 +120,14 @@ module Jobs
|
||||
topics = topics.where("#{vector_rep.topic_table_name}.topic_id IS NULL") if !force
|
||||
|
||||
ids = topics.pluck("topics.id")
|
||||
batch_size = 1000
|
||||
|
||||
ids.each_slice(batch_size) do |batch|
|
||||
vector_rep.gen_bulk_reprensentations(Topic.where(id: batch).order("topics.bumped_at DESC"))
|
||||
done += batch.length
|
||||
ids.each do |id|
|
||||
topic = Topic.find_by(id: id)
|
||||
if topic
|
||||
vector_rep.generate_representation_from(topic)
|
||||
done += 1
|
||||
end
|
||||
end
|
||||
|
||||
done
|
||||
end
|
||||
end
|
||||
|
||||
@@ -19,8 +19,6 @@ module ::Jobs
|
||||
DiscourseAi::Summarization.topic_summary(topic).force_summarize(system_user)
|
||||
end
|
||||
|
||||
return unless SiteSetting.ai_summary_gists_enabled
|
||||
|
||||
gist_t = AiSummary.summary_types[:gist]
|
||||
backfill_candidates(gist_t)
|
||||
.limit(current_budget(gist_t))
|
||||
@@ -31,9 +29,9 @@ module ::Jobs
|
||||
Topic
|
||||
.where("topics.word_count >= ?", SiteSetting.ai_summary_backfill_minimum_word_count)
|
||||
.joins(<<~SQL)
|
||||
LEFT OUTER JOIN ai_summaries ais ON
|
||||
topics.id = ais.target_id AND
|
||||
ais.target_type = 'Topic' AND
|
||||
LEFT OUTER JOIN ai_summaries ais ON
|
||||
topics.id = ais.target_id AND
|
||||
ais.target_type = 'Topic' AND
|
||||
ais.summary_type = '#{summary_type}'
|
||||
SQL
|
||||
.where(
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
class AiApiAuditLog < ActiveRecord::Base
|
||||
belongs_to :post
|
||||
belongs_to :topic
|
||||
belongs_to :user
|
||||
|
||||
module Provider
|
||||
OpenAI = 1
|
||||
@@ -44,10 +43,3 @@ end
|
||||
# feature_name :string(255)
|
||||
# language_model :string(255)
|
||||
# feature_context :jsonb
|
||||
# cached_tokens :integer
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_ai_api_audit_logs_on_created_at_and_feature_name (created_at,feature_name)
|
||||
# index_ai_api_audit_logs_on_created_at_and_language_model (created_at,language_model)
|
||||
#
|
||||
|
||||
@@ -9,23 +9,15 @@ class AiSummary < ActiveRecord::Base
|
||||
def self.store!(strategy, llm_model, summary, og_content, human:)
|
||||
content_ids = og_content.map { |c| c[:id] }
|
||||
|
||||
AiSummary
|
||||
.upsert(
|
||||
{
|
||||
target_id: strategy.target.id,
|
||||
target_type: strategy.target.class.name,
|
||||
algorithm: llm_model.name,
|
||||
content_range: (content_ids.first..content_ids.last),
|
||||
summarized_text: summary,
|
||||
original_content_sha: build_sha(content_ids.join),
|
||||
summary_type: strategy.type,
|
||||
origin: !!human ? origins[:human] : origins[:system],
|
||||
},
|
||||
unique_by: %i[target_id target_type summary_type],
|
||||
update_only: %i[summarized_text original_content_sha algorithm origin content_range],
|
||||
)
|
||||
.first
|
||||
.then { AiSummary.find_by(id: _1["id"]) }
|
||||
AiSummary.create!(
|
||||
target: strategy.target,
|
||||
algorithm: llm_model.name,
|
||||
content_range: (content_ids.first..content_ids.last),
|
||||
summarized_text: summary,
|
||||
original_content_sha: build_sha(content_ids.join),
|
||||
summary_type: strategy.type,
|
||||
origin: !!human ? origins[:human] : origins[:system],
|
||||
)
|
||||
end
|
||||
|
||||
def self.build_sha(joined_ids)
|
||||
|
||||
@@ -172,7 +172,6 @@ end
|
||||
# updated_at :datetime not null
|
||||
# force_review :boolean default(FALSE), not null
|
||||
# reject_reason :text
|
||||
# potentially_illegal :boolean default(FALSE)
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
|
||||
@@ -229,7 +229,6 @@ end
|
||||
# updated_at :datetime not null
|
||||
# force_review :boolean default(FALSE), not null
|
||||
# reject_reason :text
|
||||
# potentially_illegal :boolean default(FALSE)
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AiUsageSerializer < ApplicationSerializer
|
||||
attributes :data, :features, :models, :users, :summary, :period
|
||||
|
||||
def data
|
||||
object.tokens_by_period.as_json(
|
||||
only: %i[period total_tokens total_cached_tokens total_request_tokens total_response_tokens],
|
||||
)
|
||||
end
|
||||
|
||||
def period
|
||||
object.guess_period
|
||||
end
|
||||
|
||||
def features
|
||||
object.feature_breakdown.as_json(
|
||||
only: %i[
|
||||
feature_name
|
||||
usage_count
|
||||
total_tokens
|
||||
total_cached_tokens
|
||||
total_request_tokens
|
||||
total_response_tokens
|
||||
],
|
||||
)
|
||||
end
|
||||
|
||||
def models
|
||||
object.model_breakdown.as_json(
|
||||
only: %i[
|
||||
llm
|
||||
usage_count
|
||||
total_tokens
|
||||
total_cached_tokens
|
||||
total_request_tokens
|
||||
total_response_tokens
|
||||
],
|
||||
)
|
||||
end
|
||||
|
||||
def users
|
||||
object.user_breakdown.map do |user|
|
||||
{
|
||||
avatar_template: User.avatar_template(user.username, user.uploaded_avatar_id),
|
||||
username: user.username,
|
||||
usage_count: user.usage_count,
|
||||
total_tokens: user.total_tokens,
|
||||
total_cached_tokens: user.total_cached_tokens,
|
||||
total_request_tokens: user.total_request_tokens,
|
||||
total_response_tokens: user.total_response_tokens,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def summary
|
||||
{
|
||||
total_tokens: object.total_tokens,
|
||||
total_cached_tokens: object.total_cached_tokens,
|
||||
total_request_tokens: object.total_request_tokens,
|
||||
total_response_tokens: object.total_response_tokens,
|
||||
total_requests: object.total_requests,
|
||||
date_range: {
|
||||
start: object.start_date,
|
||||
end: object.end_date,
|
||||
},
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -18,6 +18,5 @@ export default {
|
||||
this.route("new");
|
||||
this.route("show", { path: "/:id" });
|
||||
});
|
||||
this.route("discourse-ai-usage", { path: "ai-usage" });
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,25 +1,22 @@
|
||||
import { computed } from "@ember/object";
|
||||
import { observes } from "@ember-decorators/object";
|
||||
import { computed, observer } from "@ember/object";
|
||||
import I18n from "discourse-i18n";
|
||||
import ComboBox from "select-kit/components/combo-box";
|
||||
import { selectKitOptions } from "select-kit/components/select-kit";
|
||||
|
||||
@selectKitOptions({
|
||||
filterable: true,
|
||||
})
|
||||
export default class AiLlmSelector extends ComboBox {
|
||||
@observes("attrs.disabled")
|
||||
_modelDisabledChanged() {
|
||||
export default ComboBox.extend({
|
||||
_modelDisabledChanged: observer("attrs.disabled", function () {
|
||||
this.selectKit.options.set("disabled", this.get("attrs.disabled.value"));
|
||||
}
|
||||
}),
|
||||
|
||||
@computed
|
||||
get content() {
|
||||
content: computed(function () {
|
||||
return [
|
||||
{
|
||||
id: "blank",
|
||||
name: I18n.t("discourse_ai.ai_persona.no_llm_selected"),
|
||||
},
|
||||
].concat(this.llms);
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
selectKitOptions: {
|
||||
filterable: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ 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";
|
||||
@@ -123,10 +124,8 @@ 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 class="d-admin-table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{i18n "discourse_ai.llms.display_name"}}</th>
|
||||
@@ -136,19 +135,12 @@ export default class AiLlmsListEditor extends Component {
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each @llms as |llm|}}
|
||||
<tr
|
||||
data-llm-id={{llm.name}}
|
||||
class="ai-llm-list__row d-admin-row__content"
|
||||
>
|
||||
<td class="d-admin-row__overview">
|
||||
<div class="ai-llm-list__name">
|
||||
<strong>
|
||||
{{llm.display_name}}
|
||||
</strong>
|
||||
</div>
|
||||
<div class="ai-llm-list__description">
|
||||
<tr data-llm-id={{llm.name}} class="ai-llm-list__row">
|
||||
<td class="column-name">
|
||||
<h3>{{llm.display_name}}</h3>
|
||||
<p>
|
||||
{{this.modelDescription llm}}
|
||||
</div>
|
||||
</p>
|
||||
{{#if llm.used_by}}
|
||||
<ul class="ai-llm-list-editor__usages">
|
||||
{{#each llm.used_by as |usage|}}
|
||||
@@ -157,20 +149,18 @@ export default class AiLlmsListEditor extends Component {
|
||||
</ul>
|
||||
{{/if}}
|
||||
</td>
|
||||
<td class="d-admin-row__detail">
|
||||
<div class="d-admin-row__mobile-label">
|
||||
{{i18n "discourse_ai.llms.provider"}}
|
||||
</div>
|
||||
<td class="column-provider">
|
||||
{{i18n
|
||||
(concat "discourse_ai.llms.providers." llm.provider)
|
||||
}}
|
||||
</td>
|
||||
<td class="d-admin-row__controls">
|
||||
<td class="column-edit">
|
||||
<LinkTo
|
||||
@route="adminPlugins.show.discourse-ai-llms.show"
|
||||
class="btn btn-default btn-small ai-llm-list__edit-button"
|
||||
class="btn btn-default"
|
||||
@model={{llm.id}}
|
||||
>
|
||||
{{icon "wrench"}}
|
||||
<div class="d-button-label">
|
||||
{{i18n "discourse_ai.llms.edit"}}
|
||||
</div>
|
||||
@@ -183,17 +173,7 @@ export default class AiLlmsListEditor extends Component {
|
||||
</section>
|
||||
{{/if}}
|
||||
<section class="ai-llms-list-editor__templates">
|
||||
<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"
|
||||
}}
|
||||
/>
|
||||
<AdminPageSubheader @titleLabel={{this.preconfiguredTitle}} />
|
||||
|
||||
<AdminSectionLandingWrapper
|
||||
class="ai-llms-list-editor__templates-list"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
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";
|
||||
@@ -8,13 +9,26 @@ 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 AdminConfigAreaEmptyList from "admin/components/admin-config-area-empty-list";
|
||||
import AdminPageSubheader from "admin/components/admin-page-subheader";
|
||||
import I18n from "discourse-i18n";
|
||||
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) {
|
||||
@@ -39,76 +53,74 @@ export default class AiPersonaListEditor extends Component {
|
||||
{{#if @currentPersona}}
|
||||
<AiPersonaEditor @model={{@currentPersona}} @personas={{@personas}} />
|
||||
{{else}}
|
||||
<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"
|
||||
<div class="ai-persona-list-editor__header">
|
||||
<h3>{{i18n "discourse_ai.ai_persona.short_title"}}</h3>
|
||||
{{#unless @currentPersona.isNew}}
|
||||
<LinkTo
|
||||
@route="adminPlugins.show.discourse-ai-personas.new"
|
||||
@icon="plus"
|
||||
class="ai-persona-list-editor__new-button"
|
||||
/>
|
||||
</:actions>
|
||||
</AdminPageSubheader>
|
||||
class="btn btn-small btn-primary"
|
||||
>
|
||||
{{icon "plus"}}
|
||||
<span>{{I18n.t "discourse_ai.ai_persona.new"}}</span>
|
||||
</LinkTo>
|
||||
{{/unless}}
|
||||
</div>
|
||||
|
||||
{{#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>
|
||||
</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 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>
|
||||
</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}}
|
||||
<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>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{/if}}
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -54,13 +54,8 @@ export default class AiSplitTopicSuggester extends Component {
|
||||
suggestions.includes(item.name.toLowerCase())
|
||||
);
|
||||
this.suggestions = suggestedCategories;
|
||||
} else if (this.args.mode === this.SUGGESTION_TYPES.tag) {
|
||||
this.suggestions = result.assistant.map((s) => {
|
||||
return {
|
||||
name: s.name,
|
||||
count: s.count,
|
||||
};
|
||||
});
|
||||
} else {
|
||||
this.suggestions = result.assistant.map((s) => s.name);
|
||||
}
|
||||
})
|
||||
.catch(popupAjaxError)
|
||||
@@ -137,17 +132,6 @@ export default class AiSplitTopicSuggester extends Component {
|
||||
{{on "click" (fn this.applySuggestion suggestion menu)}}
|
||||
>
|
||||
{{categoryBadge suggestion}}
|
||||
<span class="topic-count">x
|
||||
{{suggestion.totalTopicCount}}</span>
|
||||
</li>
|
||||
{{else if (eq @mode "suggest_tags")}}
|
||||
<li data-name={{suggestion.name}} data-value={{index}}>
|
||||
<DButton
|
||||
@translatedLabel={{suggestion.name}}
|
||||
@action={{fn this.applySuggestion suggestion.name menu}}
|
||||
>
|
||||
<span class="topic-count">x{{suggestion.count}}</span>
|
||||
</DButton>
|
||||
</li>
|
||||
{{else}}
|
||||
<li data-name={{suggestion}} data-value={{index}}>
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { fn } from "@ember/helper";
|
||||
import { action } from "@ember/object";
|
||||
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
||||
import { service } from "@ember/service";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
import I18n from "I18n";
|
||||
|
||||
export default class AISuggestionDropdown extends Component {
|
||||
@service dialog;
|
||||
@service siteSettings;
|
||||
@service composer;
|
||||
|
||||
@tracked loading = false;
|
||||
@tracked showMenu = false;
|
||||
@tracked generatedSuggestions = [];
|
||||
@tracked suggestIcon = "discourse-sparkles";
|
||||
@tracked showErrors = false;
|
||||
@tracked error = "";
|
||||
|
||||
SUGGESTION_TYPES = {
|
||||
title: "suggest_title",
|
||||
category: "suggest_category",
|
||||
tag: "suggest_tags",
|
||||
};
|
||||
|
||||
willDestroy() {
|
||||
super.willDestroy(...arguments);
|
||||
document.removeEventListener("click", this.onClickOutside);
|
||||
}
|
||||
|
||||
get showAIButton() {
|
||||
const minCharacterCount = 40;
|
||||
const isShowAIButton = this.composer.model.replyLength > minCharacterCount;
|
||||
const composerFields = document.querySelector(".composer-fields");
|
||||
|
||||
if (composerFields) {
|
||||
if (isShowAIButton) {
|
||||
composerFields.classList.add("showing-ai-suggestions");
|
||||
} else {
|
||||
composerFields.classList.remove("showing-ai-suggestions");
|
||||
}
|
||||
}
|
||||
|
||||
return isShowAIButton;
|
||||
}
|
||||
|
||||
get disableSuggestionButton() {
|
||||
return this.loading;
|
||||
}
|
||||
|
||||
@action
|
||||
applyClasses() {
|
||||
if (this.showAIButton) {
|
||||
document
|
||||
.querySelector(".composer-fields")
|
||||
?.classList.add("showing-ai-suggestions");
|
||||
} else {
|
||||
document
|
||||
.querySelector(".composer-fields")
|
||||
?.classList.remove("showing-ai-suggestions");
|
||||
}
|
||||
}
|
||||
|
||||
@bind
|
||||
onClickOutside(event) {
|
||||
const menu = document.querySelector(".ai-title-suggestions-menu");
|
||||
|
||||
if (event.target === menu) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.#closeMenu();
|
||||
}
|
||||
|
||||
@action
|
||||
handleClickOutside() {
|
||||
document.addEventListener("click", this.onClickOutside);
|
||||
}
|
||||
|
||||
@action
|
||||
applySuggestion(suggestion) {
|
||||
if (!this.args.mode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const composer = this.args?.composer;
|
||||
if (!composer) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.args.mode === this.SUGGESTION_TYPES.title) {
|
||||
composer.set("title", suggestion);
|
||||
return this.#closeMenu();
|
||||
}
|
||||
|
||||
if (this.args.mode === this.SUGGESTION_TYPES.category) {
|
||||
const selectedCategoryId = this.composer.categories.find(
|
||||
(c) => c.slug === suggestion
|
||||
).id;
|
||||
composer.set("categoryId", selectedCategoryId);
|
||||
return this.#closeMenu();
|
||||
}
|
||||
|
||||
if (this.args.mode === this.SUGGESTION_TYPES.tag) {
|
||||
this.#updateTags(suggestion, composer);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
async performSuggestion() {
|
||||
if (!this.args.mode) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.composer.model.replyLength === 0) {
|
||||
return this.dialog.alert(
|
||||
I18n.t("discourse_ai.ai_helper.missing_content")
|
||||
);
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
this.suggestIcon = "spinner";
|
||||
|
||||
return ajax(`/discourse-ai/ai-helper/${this.args.mode}`, {
|
||||
method: "POST",
|
||||
data: { text: this.composer.model.reply },
|
||||
})
|
||||
.then((data) => {
|
||||
this.#assignGeneratedSuggestions(data, this.args.mode);
|
||||
})
|
||||
.catch(popupAjaxError)
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
this.suggestIcon = "sync-alt";
|
||||
this.showMenu = true;
|
||||
|
||||
if (this.args.mode === "suggest_category") {
|
||||
document
|
||||
.querySelector(".category-input")
|
||||
?.classList.add("showing-ai-suggestion-menu");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#closeMenu() {
|
||||
if (this.showMenu && this.args.mode === "suggest_category") {
|
||||
document
|
||||
.querySelector(".category-input")
|
||||
?.classList.remove("showing-ai-suggestion-menu");
|
||||
}
|
||||
|
||||
this.suggestIcon = "discourse-sparkles";
|
||||
this.showMenu = false;
|
||||
this.showErrors = false;
|
||||
this.errors = "";
|
||||
}
|
||||
|
||||
#updateTags(suggestion, composer) {
|
||||
const maxTags = this.siteSettings.max_tags_per_topic;
|
||||
|
||||
if (!composer.tags) {
|
||||
composer.set("tags", [suggestion]);
|
||||
// remove tag from the list of suggestions once added
|
||||
this.generatedSuggestions = this.generatedSuggestions.filter(
|
||||
(s) => s !== suggestion
|
||||
);
|
||||
return;
|
||||
}
|
||||
const tags = composer.tags;
|
||||
|
||||
if (tags?.length >= maxTags) {
|
||||
// Show error if trying to add more tags than allowed
|
||||
this.showErrors = true;
|
||||
this.error = I18n.t("select_kit.max_content_reached", { count: maxTags });
|
||||
return;
|
||||
}
|
||||
|
||||
tags.push(suggestion);
|
||||
composer.set("tags", [...tags]);
|
||||
// remove tag from the list of suggestions once added
|
||||
return (this.generatedSuggestions = this.generatedSuggestions.filter(
|
||||
(s) => s !== suggestion
|
||||
));
|
||||
}
|
||||
|
||||
#tagSelectorHasValues() {
|
||||
return this.args.composer?.tags && this.args.composer?.tags.length > 0;
|
||||
}
|
||||
|
||||
#assignGeneratedSuggestions(data, mode) {
|
||||
if (mode === this.SUGGESTION_TYPES.title) {
|
||||
return (this.generatedSuggestions = data.suggestions);
|
||||
}
|
||||
|
||||
const suggestions = data.assistant.map((s) => s.name);
|
||||
|
||||
if (mode === this.SUGGESTION_TYPES.tag) {
|
||||
if (this.#tagSelectorHasValues()) {
|
||||
// Filter out tags if they are already selected in the tag input
|
||||
return (this.generatedSuggestions = suggestions.filter(
|
||||
(t) => !this.args.composer.tags.includes(t)
|
||||
));
|
||||
} else {
|
||||
return (this.generatedSuggestions = suggestions);
|
||||
}
|
||||
}
|
||||
|
||||
return (this.generatedSuggestions = suggestions);
|
||||
}
|
||||
|
||||
<template>
|
||||
{{#if this.showAIButton}}
|
||||
<DButton
|
||||
@icon={{this.suggestIcon}}
|
||||
@title="discourse_ai.ai_helper.suggest"
|
||||
@action={{this.performSuggestion}}
|
||||
@disabled={{this.disableSuggestionButton}}
|
||||
class="suggestion-button {{if this.loading 'is-loading'}}"
|
||||
...attributes
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.showMenu}}
|
||||
{{! template-lint-disable modifier-name-case }}
|
||||
<ul
|
||||
class="popup-menu ai-suggestions-menu"
|
||||
{{didInsert this.handleClickOutside}}
|
||||
>
|
||||
{{#if this.showErrors}}
|
||||
<li class="ai-suggestions-menu__errors">{{this.error}}</li>
|
||||
{{/if}}
|
||||
{{#each this.generatedSuggestions as |suggestion index|}}
|
||||
<li data-name={{suggestion}} data-value={{index}}>
|
||||
<DButton
|
||||
@translatedLabel={{suggestion}}
|
||||
@action={{fn this.applySuggestion suggestion}}
|
||||
class="popup-menu-btn"
|
||||
/>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/if}}
|
||||
</template>
|
||||
}
|
||||
@@ -2,10 +2,9 @@ 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;
|
||||
@@ -16,64 +15,44 @@ export default class AiToolListEditor extends Component {
|
||||
@label={{i18n "discourse_ai.tools.short_title"}}
|
||||
/>
|
||||
<section class="ai-tool-list-editor__current admin-detail pull-left">
|
||||
<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>
|
||||
<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>
|
||||
|
||||
{{#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>
|
||||
<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>
|
||||
</div>
|
||||
</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}}
|
||||
<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>
|
||||
</section>
|
||||
</template>
|
||||
}
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import { observer } from "@ember/object";
|
||||
import { readOnly } from "@ember/object/computed";
|
||||
import { observes } from "@ember-decorators/object";
|
||||
import MultiSelectComponent from "select-kit/components/multi-select";
|
||||
import { selectKitOptions } from "select-kit/components/select-kit";
|
||||
|
||||
@selectKitOptions({
|
||||
filterable: true,
|
||||
})
|
||||
export default class AiToolSelector extends MultiSelectComponent {
|
||||
@readOnly("tools") content;
|
||||
|
||||
value = "";
|
||||
|
||||
@observes("attrs.disabled")
|
||||
_modelDisabledChanged() {
|
||||
export default MultiSelectComponent.extend({
|
||||
_modelDisabledChanged: observer("attrs.disabled", function () {
|
||||
this.selectKit.options.set("disabled", this.get("attrs.disabled.value"));
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
content: readOnly("tools"),
|
||||
|
||||
value: "",
|
||||
|
||||
selectKitOptions: {
|
||||
filterable: true,
|
||||
},
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,154 +0,0 @@
|
||||
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";
|
||||
import { service } from "@ember/service";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import DropdownMenu from "discourse/components/dropdown-menu";
|
||||
import categoryBadge from "discourse/helpers/category-badge";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
import DMenu from "float-kit/components/d-menu";
|
||||
import { MIN_CHARACTER_COUNT } from "../../lib/ai-helper-suggestions";
|
||||
|
||||
export default class AiCategorySuggester extends Component {
|
||||
@service siteSettings;
|
||||
@tracked loading = false;
|
||||
@tracked suggestions = null;
|
||||
@tracked untriggers = [];
|
||||
@tracked triggerIcon = "discourse-sparkles";
|
||||
@tracked content = null;
|
||||
@tracked topicContent = null;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
if (!this.topicContent && this.args.composer?.reply === undefined) {
|
||||
this.fetchTopicContent();
|
||||
}
|
||||
}
|
||||
|
||||
async fetchTopicContent() {
|
||||
await ajax(`/t/${this.args.buffered.content.id}.json`).then(
|
||||
({ post_stream }) => {
|
||||
this.topicContent = post_stream.posts[0].cooked;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
get showSuggestionButton() {
|
||||
const composerFields = document.querySelector(".composer-fields");
|
||||
this.content = this.args.composer?.reply || this.topicContent;
|
||||
const showTrigger = this.content?.length > MIN_CHARACTER_COUNT;
|
||||
|
||||
if (composerFields) {
|
||||
if (showTrigger) {
|
||||
composerFields.classList.add("showing-ai-suggestions");
|
||||
} else {
|
||||
composerFields.classList.remove("showing-ai-suggestions");
|
||||
}
|
||||
}
|
||||
|
||||
return this.siteSettings.ai_embeddings_enabled && showTrigger;
|
||||
}
|
||||
|
||||
@action
|
||||
async loadSuggestions() {
|
||||
if (this.suggestions && !this.dMenu.expanded) {
|
||||
return this.suggestions;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
this.triggerIcon = "spinner";
|
||||
|
||||
try {
|
||||
const { assistant } = await ajax(
|
||||
"/discourse-ai/ai-helper/suggest_category",
|
||||
{
|
||||
method: "POST",
|
||||
data: { text: this.content },
|
||||
}
|
||||
);
|
||||
this.suggestions = assistant;
|
||||
} catch (error) {
|
||||
popupAjaxError(error);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
this.triggerIcon = "sync-alt";
|
||||
}
|
||||
|
||||
return this.suggestions;
|
||||
}
|
||||
|
||||
@action
|
||||
applySuggestion(suggestion) {
|
||||
const composer = this.args.composer;
|
||||
const buffered = this.args.buffered;
|
||||
|
||||
if (composer) {
|
||||
composer.set("categoryId", suggestion.id);
|
||||
composer.get("categoryId");
|
||||
}
|
||||
|
||||
if (buffered) {
|
||||
this.args.buffered.set("category_id", suggestion.id);
|
||||
}
|
||||
|
||||
return this.dMenu.close();
|
||||
}
|
||||
|
||||
@action
|
||||
onRegisterApi(api) {
|
||||
this.dMenu = api;
|
||||
}
|
||||
|
||||
@action
|
||||
onClose() {
|
||||
this.triggerIcon = "discourse-sparkles";
|
||||
}
|
||||
|
||||
<template>
|
||||
{{#if this.showSuggestionButton}}
|
||||
<DMenu
|
||||
@title={{i18n "discourse_ai.ai_helper.suggest"}}
|
||||
@icon={{this.triggerIcon}}
|
||||
@identifier="ai-category-suggester"
|
||||
@onClose={{this.onClose}}
|
||||
@triggerClass="suggestion-button suggest-category-button {{if
|
||||
this.loading
|
||||
'is-loading'
|
||||
}}"
|
||||
@contentClass="ai-suggestions-menu"
|
||||
@onRegisterApi={{this.onRegisterApi}}
|
||||
@modalForMobile={{true}}
|
||||
@untriggers={{this.untriggers}}
|
||||
{{on "click" this.loadSuggestions}}
|
||||
>
|
||||
<:content>
|
||||
{{#unless this.loading}}
|
||||
<DropdownMenu as |dropdown|>
|
||||
{{#each this.suggestions as |suggestion index|}}
|
||||
<dropdown.item>
|
||||
<DButton
|
||||
class="category-row"
|
||||
data-name={{suggestion.name}}
|
||||
data-value={{index}}
|
||||
title={{suggestion.name}}
|
||||
@action={{fn this.applySuggestion suggestion}}
|
||||
>
|
||||
<div class="category-status">
|
||||
{{categoryBadge suggestion}}
|
||||
<span class="topic-count">x
|
||||
{{suggestion.topicCount}}</span>
|
||||
</div>
|
||||
</DButton>
|
||||
</dropdown.item>
|
||||
{{/each}}
|
||||
</DropdownMenu>
|
||||
{{/unless}}
|
||||
</:content>
|
||||
</DMenu>
|
||||
{{/if}}
|
||||
</template>
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user