Compare commits
25
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1598b4679 | ||
|
|
65bbcd71fc | ||
|
|
d07cf51653 | ||
|
|
20612fde52 | ||
|
|
7e9c0dc076 | ||
|
|
2d1ce01320 | ||
|
|
b24669c810 | ||
|
|
24b69bf840 | ||
|
|
404092a68c | ||
|
|
327adbde29 | ||
|
|
749af40fad | ||
|
|
61758ff8a6 | ||
|
|
b6cefd10fa | ||
|
|
11d0f60f1e | ||
|
|
f9f89adac5 | ||
|
|
b480f13a0f | ||
|
|
47ecf86aa1 | ||
|
|
792df58fbc | ||
|
|
ceac6e5efb | ||
|
|
bdb8f1d5e0 | ||
|
|
d15876025f | ||
|
|
7607477ff9 | ||
|
|
059b3fabb8 | ||
|
|
6a7a45fd4f | ||
|
|
ac705b694b |
@@ -1,3 +1,4 @@
|
||||
< 3.4.0.beta4-dev: 20612fde52d3f740cad64823ef8aadb0748b567f
|
||||
< 3.4.0.beta3-dev: decf1bb49d737ea15308400f22f89d1d1e71d13d
|
||||
< 3.4.0.beta1-dev: 9d887ad4ace8e33c3fe7dbb39237e882c08b4f0b
|
||||
< 3.3.0.beta5-dev: 4d8090002f6dcd8e34d41033606bf131fa221475
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module DiscourseAi
|
||||
module Admin
|
||||
class AiLlmQuotasController < ::Admin::AdminController
|
||||
requires_plugin ::DiscourseAi::PLUGIN_NAME
|
||||
|
||||
def index
|
||||
quotas = LlmQuota.includes(:group)
|
||||
|
||||
render json: {
|
||||
quotas:
|
||||
ActiveModel::ArraySerializer.new(quotas, each_serializer: LlmQuotaSerializer),
|
||||
}
|
||||
end
|
||||
|
||||
def create
|
||||
quota = LlmQuota.new(quota_params)
|
||||
|
||||
if quota.save
|
||||
render json: LlmQuotaSerializer.new(quota), status: :created
|
||||
else
|
||||
render_json_error quota
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
quota = LlmQuota.find(params[:id])
|
||||
|
||||
if quota.update(quota_params)
|
||||
render json: LlmQuotaSerializer.new(quota)
|
||||
else
|
||||
render_json_error quota
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
quota = LlmQuota.find(params[:id])
|
||||
quota.destroy!
|
||||
|
||||
head :no_content
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render json: { error: I18n.t("not_found") }, status: 404
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def quota_params
|
||||
params.require(:quota).permit(
|
||||
:group_id,
|
||||
:llm_model_id,
|
||||
:max_tokens,
|
||||
:max_usages,
|
||||
:duration_seconds,
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -6,7 +6,7 @@ module DiscourseAi
|
||||
requires_plugin ::DiscourseAi::PLUGIN_NAME
|
||||
|
||||
def index
|
||||
llms = LlmModel.all.order(:display_name)
|
||||
llms = LlmModel.all.includes(:llm_quotas).order(:display_name)
|
||||
|
||||
render json: {
|
||||
ai_llms:
|
||||
@@ -40,6 +40,11 @@ module DiscourseAi
|
||||
|
||||
def create
|
||||
llm_model = LlmModel.new(ai_llm_params)
|
||||
|
||||
# we could do nested attributes but the mechanics are not ideal leading
|
||||
# to lots of complex debugging, this is simpler
|
||||
quota_params.each { |quota| llm_model.llm_quotas.build(quota) } if quota_params
|
||||
|
||||
if llm_model.save
|
||||
llm_model.toggle_companion_user
|
||||
render json: LlmModelSerializer.new(llm_model), status: :created
|
||||
@@ -51,6 +56,25 @@ module DiscourseAi
|
||||
def update
|
||||
llm_model = LlmModel.find(params[:id])
|
||||
|
||||
if params[:ai_llm].key?(:llm_quotas)
|
||||
if quota_params
|
||||
existing_quota_group_ids = llm_model.llm_quotas.pluck(:group_id)
|
||||
new_quota_group_ids = quota_params.map { |q| q[:group_id] }
|
||||
|
||||
llm_model
|
||||
.llm_quotas
|
||||
.where(group_id: existing_quota_group_ids - new_quota_group_ids)
|
||||
.destroy_all
|
||||
|
||||
quota_params.each do |quota_param|
|
||||
quota = llm_model.llm_quotas.find_or_initialize_by(group_id: quota_param[:group_id])
|
||||
quota.update!(quota_param)
|
||||
end
|
||||
else
|
||||
llm_model.llm_quotas.destroy_all
|
||||
end
|
||||
end
|
||||
|
||||
if llm_model.seeded?
|
||||
return render_json_error(I18n.t("discourse_ai.llm.cannot_edit_builtin"), status: 403)
|
||||
end
|
||||
@@ -110,6 +134,19 @@ module DiscourseAi
|
||||
|
||||
private
|
||||
|
||||
def quota_params
|
||||
if params[:ai_llm][:llm_quotas].present?
|
||||
params[:ai_llm][:llm_quotas].map do |quota|
|
||||
mapped = {}
|
||||
mapped[:group_id] = quota[:group_id].to_i
|
||||
mapped[:max_tokens] = quota[:max_tokens].to_i if quota[:max_tokens].present?
|
||||
mapped[:max_usages] = quota[:max_usages].to_i if quota[:max_usages].present?
|
||||
mapped[:duration_seconds] = quota[:duration_seconds].to_i
|
||||
mapped
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def ai_llm_params(updating: nil)
|
||||
return {} if params[:ai_llm].blank?
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ module DiscourseAi
|
||||
llm_model_id = updated_params[:llm_model_id] = allowed_params[:llm_model_id]
|
||||
if llm_model_id.to_i < 0 &&
|
||||
!SiteSetting.ai_spam_detection_model_allowed_seeded_models_map.include?(
|
||||
"custom:#{llm_model_id}",
|
||||
llm_model_id.to_s,
|
||||
)
|
||||
return(
|
||||
render_json_error(
|
||||
@@ -86,6 +86,31 @@ module DiscourseAi
|
||||
render json: result
|
||||
end
|
||||
|
||||
def fix_errors
|
||||
case params[:error]
|
||||
when "spam_scanner_not_admin"
|
||||
begin
|
||||
DiscourseAi::AiModeration::SpamScanner.fix_spam_scanner_not_admin
|
||||
render json: success_json
|
||||
rescue ActiveRecord::RecordInvalid
|
||||
render_json_error(
|
||||
I18n.t("discourse_ai.spam_detection.bot_user_update_failed"),
|
||||
status: :unprocessable_entity,
|
||||
)
|
||||
rescue StandardError
|
||||
render_json_error(
|
||||
I18n.t("discourse_ai.spam_detection.unexpected"),
|
||||
status: :internal_server_error,
|
||||
)
|
||||
end
|
||||
else
|
||||
render_json_error(
|
||||
I18n.t("discourse_ai.spam_detection.invalid_error_type"),
|
||||
status: :bad_request,
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def allowed_params
|
||||
|
||||
@@ -57,6 +57,7 @@ module DiscourseAi
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>#{ERB::Util.html_escape(name)}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, user-scalable=yes, viewport-fit=cover, interactive-widget=resizes-content">
|
||||
<style>
|
||||
html, body, iframe {
|
||||
margin: 0;
|
||||
|
||||
@@ -4,6 +4,7 @@ class LlmModel < ActiveRecord::Base
|
||||
FIRST_BOT_USER_ID = -1200
|
||||
BEDROCK_PROVIDER_NAME = "aws_bedrock"
|
||||
|
||||
has_many :llm_quotas, dependent: :destroy
|
||||
belongs_to :user
|
||||
|
||||
validates :display_name, presence: true, length: { maximum: 100 }
|
||||
@@ -32,6 +33,7 @@ class LlmModel < ActiveRecord::Base
|
||||
open_ai: {
|
||||
organization: :text,
|
||||
disable_native_tools: :checkbox,
|
||||
disable_streaming: :checkbox,
|
||||
},
|
||||
mistral: {
|
||||
disable_native_tools: :checkbox,
|
||||
@@ -51,11 +53,13 @@ class LlmModel < ActiveRecord::Base
|
||||
ollama: {
|
||||
disable_system_prompt: :checkbox,
|
||||
enable_native_tool: :checkbox,
|
||||
disable_streaming: :checkbox,
|
||||
},
|
||||
open_router: {
|
||||
disable_native_tools: :checkbox,
|
||||
provider_order: :text,
|
||||
provider_quantizations: :text,
|
||||
disable_streaming: :checkbox,
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class LlmQuota < ActiveRecord::Base
|
||||
self.table_name = "llm_quotas"
|
||||
|
||||
belongs_to :group
|
||||
belongs_to :llm_model
|
||||
has_many :llm_quota_usages
|
||||
|
||||
validates :group_id, presence: true
|
||||
# we can not validate on create cause it breaks build
|
||||
validates :llm_model_id, presence: true, on: :update
|
||||
validates :duration_seconds, presence: true, numericality: { greater_than: 0 }
|
||||
validates :max_tokens, numericality: { only_integer: true, greater_than: 0, allow_nil: true }
|
||||
validates :max_usages, numericality: { greater_than: 0, allow_nil: true }
|
||||
|
||||
validate :at_least_one_limit
|
||||
|
||||
def self.check_quotas!(llm, user)
|
||||
return true if user.blank?
|
||||
quotas = joins(:group).where(llm_model: llm).where(group: user.groups)
|
||||
|
||||
return true if quotas.empty?
|
||||
errors =
|
||||
quotas.map do |quota|
|
||||
usage = LlmQuotaUsage.find_or_create_for(user: user, llm_quota: quota)
|
||||
begin
|
||||
usage.check_quota!
|
||||
nil
|
||||
rescue LlmQuotaUsage::QuotaExceededError => e
|
||||
e
|
||||
end
|
||||
end
|
||||
|
||||
return if errors.include?(nil)
|
||||
|
||||
raise errors.first
|
||||
end
|
||||
|
||||
def self.log_usage(llm, user, input_tokens, output_tokens)
|
||||
return if user.blank?
|
||||
|
||||
quotas = joins(:group).where(llm_model: llm).where(group: user.groups)
|
||||
|
||||
quotas.each do |quota|
|
||||
usage = LlmQuotaUsage.find_or_create_for(user: user, llm_quota: quota)
|
||||
usage.increment_usage!(input_tokens: input_tokens, output_tokens: output_tokens)
|
||||
end
|
||||
end
|
||||
|
||||
def available_tokens
|
||||
max_tokens
|
||||
end
|
||||
|
||||
def available_usages
|
||||
max_usages
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def at_least_one_limit
|
||||
if max_tokens.nil? && max_usages.nil?
|
||||
errors.add(:base, I18n.t("discourse_ai.errors.quota_required"))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: llm_quotas
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# group_id :bigint not null
|
||||
# llm_model_id :bigint not null
|
||||
# max_tokens :integer
|
||||
# max_usages :integer
|
||||
# duration_seconds :integer not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_llm_quotas_on_group_id_and_llm_model_id (group_id,llm_model_id) UNIQUE
|
||||
# index_llm_quotas_on_llm_model_id (llm_model_id)
|
||||
#
|
||||
@@ -0,0 +1,120 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class LlmQuotaUsage < ActiveRecord::Base
|
||||
self.table_name = "llm_quota_usages"
|
||||
|
||||
QuotaExceededError = Class.new(StandardError)
|
||||
|
||||
belongs_to :user
|
||||
belongs_to :llm_quota
|
||||
|
||||
validates :user_id, presence: true
|
||||
validates :llm_quota_id, presence: true
|
||||
validates :input_tokens_used, presence: true, numericality: { greater_than_or_equal_to: 0 }
|
||||
validates :output_tokens_used, presence: true, numericality: { greater_than_or_equal_to: 0 }
|
||||
validates :usages, presence: true, numericality: { greater_than_or_equal_to: 0 }
|
||||
validates :started_at, presence: true
|
||||
validates :reset_at, presence: true
|
||||
|
||||
def self.find_or_create_for(user:, llm_quota:)
|
||||
usage = find_or_initialize_by(user: user, llm_quota: llm_quota)
|
||||
|
||||
if usage.new_record?
|
||||
now = Time.current
|
||||
usage.started_at = now
|
||||
usage.reset_at = now + llm_quota.duration_seconds.seconds
|
||||
usage.input_tokens_used = 0
|
||||
usage.output_tokens_used = 0
|
||||
usage.usages = 0
|
||||
usage.save!
|
||||
end
|
||||
|
||||
usage
|
||||
end
|
||||
|
||||
def reset_if_needed!
|
||||
return if Time.current < reset_at
|
||||
|
||||
now = Time.current
|
||||
update!(
|
||||
input_tokens_used: 0,
|
||||
output_tokens_used: 0,
|
||||
usages: 0,
|
||||
started_at: now,
|
||||
reset_at: now + llm_quota.duration_seconds.seconds,
|
||||
)
|
||||
end
|
||||
|
||||
def increment_usage!(input_tokens:, output_tokens:)
|
||||
reset_if_needed!
|
||||
|
||||
increment!(:usages)
|
||||
increment!(:input_tokens_used, input_tokens)
|
||||
increment!(:output_tokens_used, output_tokens)
|
||||
end
|
||||
|
||||
def check_quota!
|
||||
reset_if_needed!
|
||||
|
||||
if quota_exceeded?
|
||||
raise QuotaExceededError.new(
|
||||
I18n.t(
|
||||
"discourse_ai.errors.quota_exceeded",
|
||||
relative_time: AgeWords.distance_of_time_in_words(reset_at, Time.now),
|
||||
),
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def quota_exceeded?
|
||||
return false if !llm_quota
|
||||
|
||||
(llm_quota.max_tokens.present? && total_tokens_used > llm_quota.max_tokens) ||
|
||||
(llm_quota.max_usages.present? && usages > llm_quota.max_usages)
|
||||
end
|
||||
|
||||
def total_tokens_used
|
||||
input_tokens_used + output_tokens_used
|
||||
end
|
||||
|
||||
def remaining_tokens
|
||||
return nil if llm_quota.max_tokens.nil?
|
||||
[0, llm_quota.max_tokens - total_tokens_used].max
|
||||
end
|
||||
|
||||
def remaining_usages
|
||||
return nil if llm_quota.max_usages.nil?
|
||||
[0, llm_quota.max_usages - usages].max
|
||||
end
|
||||
|
||||
def percentage_tokens_used
|
||||
return 0 if llm_quota.max_tokens.nil? || llm_quota.max_tokens.zero?
|
||||
[(total_tokens_used.to_f / llm_quota.max_tokens * 100).round, 100].min
|
||||
end
|
||||
|
||||
def percentage_usages_used
|
||||
return 0 if llm_quota.max_usages.nil? || llm_quota.max_usages.zero?
|
||||
[(usages.to_f / llm_quota.max_usages * 100).round, 100].min
|
||||
end
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: llm_quota_usages
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# user_id :bigint not null
|
||||
# llm_quota_id :bigint not null
|
||||
# input_tokens_used :integer not null
|
||||
# output_tokens_used :integer not null
|
||||
# usages :integer not null
|
||||
# started_at :datetime not null
|
||||
# reset_at :datetime not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_llm_quota_usages_on_llm_quota_id (llm_quota_id)
|
||||
# index_llm_quota_usages_on_user_id_and_llm_quota_id (user_id,llm_quota_id) UNIQUE
|
||||
#
|
||||
@@ -7,7 +7,8 @@ class AiSpamSerializer < ApplicationSerializer
|
||||
:available_llms,
|
||||
:stats,
|
||||
:flagging_username,
|
||||
:spam_score_type
|
||||
:spam_score_type,
|
||||
:spam_scanning_user
|
||||
|
||||
def is_enabled
|
||||
object[:enabled]
|
||||
@@ -47,4 +48,10 @@ class AiSpamSerializer < ApplicationSerializer
|
||||
def settings
|
||||
object[:settings]
|
||||
end
|
||||
|
||||
def spam_scanning_user
|
||||
user = DiscourseAi::AiModeration::SpamScanner.flagging_user
|
||||
|
||||
user.serializable_hash(only: %i[id username name admin]) if user.present?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -20,6 +20,7 @@ class LlmModelSerializer < ApplicationSerializer
|
||||
:used_by
|
||||
|
||||
has_one :user, serializer: BasicUserSerializer, embed: :object
|
||||
has_many :llm_quotas, serializer: LlmQuotaSerializer, embed: :objects
|
||||
|
||||
def used_by
|
||||
llm_usage =
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class LlmQuotaSerializer < ApplicationSerializer
|
||||
attributes :id, :group_id, :llm_model_id, :max_tokens, :max_usages, :duration_seconds, :group_name
|
||||
|
||||
def group_name
|
||||
object.group.name
|
||||
end
|
||||
end
|
||||
@@ -21,7 +21,7 @@ export default class AiLlm extends RestModel {
|
||||
updateProperties() {
|
||||
const attrs = this.createProperties();
|
||||
attrs.id = this.id;
|
||||
|
||||
attrs.llm_quotas = this.llm_quotas;
|
||||
return attrs;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { tracked } from "@glimmer/tracking";
|
||||
import { action } from "@ember/object";
|
||||
import { getOwner } from "@ember/owner";
|
||||
import { service } from "@ember/service";
|
||||
import I18n from "discourse-i18n";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import DToast from "float-kit/components/d-toast";
|
||||
import DToastInstance from "float-kit/lib/d-toast-instance";
|
||||
import AiHelperOptionsList from "../components/ai-helper-options-list";
|
||||
@@ -45,7 +45,7 @@ export default class AiComposerHelperMenu extends Component {
|
||||
this.siteSettings.available_locales
|
||||
);
|
||||
const locale = availableLocales.find((l) => l.value === siteLocale);
|
||||
const translatedName = I18n.t(
|
||||
const translatedName = i18n(
|
||||
"discourse_ai.ai_helper.context_menu.translate_prompt",
|
||||
{
|
||||
language: locale.name,
|
||||
@@ -90,7 +90,7 @@ export default class AiComposerHelperMenu extends Component {
|
||||
data: {
|
||||
theme: "error",
|
||||
icon: "triangle-exclamation",
|
||||
message: I18n.t("discourse_ai.ai_helper.no_content_error"),
|
||||
message: i18n("discourse_ai.ai_helper.no_content_error"),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { computed } from "@ember/object";
|
||||
import I18n from "discourse-i18n";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import ComboBox from "select-kit/components/combo-box";
|
||||
|
||||
export default ComboBox.extend({
|
||||
@@ -7,14 +7,14 @@ export default ComboBox.extend({
|
||||
const content = [
|
||||
{
|
||||
id: -1,
|
||||
name: I18n.t("discourse_ai.ai_persona.tool_strategies.all"),
|
||||
name: i18n("discourse_ai.ai_persona.tool_strategies.all"),
|
||||
},
|
||||
];
|
||||
|
||||
[1, 2, 5].forEach((i) => {
|
||||
content.push({
|
||||
id: i,
|
||||
name: I18n.t("discourse_ai.ai_persona.tool_strategies.replies", {
|
||||
name: i18n("discourse_ai.ai_persona.tool_strategies.replies", {
|
||||
count: i,
|
||||
}),
|
||||
});
|
||||
|
||||
+71
-43
@@ -6,18 +6,17 @@ import didUpdate from "@ember/render-modifiers/modifiers/did-update";
|
||||
import { service } from "@ember/service";
|
||||
import DToggleSwitch from "discourse/components/d-toggle-switch";
|
||||
import { SEARCH_TYPE_DEFAULT } from "discourse/controllers/full-page-search";
|
||||
import concatClass from "discourse/helpers/concat-class";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { isValidSearchTerm, translateResults } from "discourse/lib/search";
|
||||
import icon from "discourse-common/helpers/d-icon";
|
||||
import I18n, { i18n } from "discourse-i18n";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import DTooltip from "float-kit/components/d-tooltip";
|
||||
import AiIndicatorWave from "../../components/ai-indicator-wave";
|
||||
import AiIndicatorWave from "./ai-indicator-wave";
|
||||
|
||||
export default class SemanticSearch extends Component {
|
||||
static shouldRender(_args, { siteSettings }) {
|
||||
return siteSettings.ai_embeddings_semantic_search_enabled;
|
||||
}
|
||||
const AI_RESULTS_TOGGLED = "full-page-search:ai-results-toggled";
|
||||
|
||||
export default class AiFullPageSearch extends Component {
|
||||
@service appEvents;
|
||||
@service router;
|
||||
@service siteSettings;
|
||||
@@ -26,8 +25,8 @@ export default class SemanticSearch extends Component {
|
||||
@tracked searching;
|
||||
@tracked AiResults = [];
|
||||
@tracked showingAiResults = false;
|
||||
@tracked sortOrder = this.args.outletArgs.sortOrder;
|
||||
initialSearchTerm = this.args.outletArgs.search;
|
||||
@tracked sortOrder = this.args.sortOrder;
|
||||
initialSearchTerm = this.args.searchTerm;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
@@ -46,7 +45,7 @@ export default class SemanticSearch extends Component {
|
||||
return;
|
||||
}
|
||||
|
||||
this.initialSearchTerm = this.args.outletArgs.search;
|
||||
this.initialSearchTerm = this.args.searchTerm;
|
||||
this.searching = true;
|
||||
this.resetAiResults();
|
||||
return this.performHyDESearch();
|
||||
@@ -68,26 +67,26 @@ export default class SemanticSearch extends Component {
|
||||
|
||||
get searchStateText() {
|
||||
if (!this.validSearchOrder) {
|
||||
return I18n.t(
|
||||
return i18n(
|
||||
"discourse_ai.embeddings.semantic_search_results.unavailable"
|
||||
);
|
||||
}
|
||||
|
||||
// Search loading:
|
||||
if (this.searching) {
|
||||
return I18n.t("discourse_ai.embeddings.semantic_search_loading");
|
||||
return i18n("discourse_ai.embeddings.semantic_search_loading");
|
||||
}
|
||||
|
||||
// We have results and we are showing them
|
||||
if (this.AiResults.length && this.showingAiResults) {
|
||||
return I18n.t("discourse_ai.embeddings.semantic_search_results.toggle", {
|
||||
return i18n("discourse_ai.embeddings.semantic_search_results.toggle", {
|
||||
count: this.AiResults.length,
|
||||
});
|
||||
}
|
||||
|
||||
// We have results but are hiding them
|
||||
if (this.AiResults.length && !this.showingAiResults) {
|
||||
return I18n.t(
|
||||
return i18n(
|
||||
"discourse_ai.embeddings.semantic_search_results.toggle_hidden",
|
||||
{
|
||||
count: this.AiResults.length,
|
||||
@@ -100,37 +99,70 @@ export default class SemanticSearch extends Component {
|
||||
this.AiResults.length === 0 &&
|
||||
this.searchTerm !== this.initialSearchTerm
|
||||
) {
|
||||
return I18n.t("discourse_ai.embeddings.semantic_search_results.new");
|
||||
return i18n("discourse_ai.embeddings.semantic_search_results.new");
|
||||
}
|
||||
|
||||
// No results:
|
||||
if (this.AiResults.length === 0) {
|
||||
return I18n.t("discourse_ai.embeddings.semantic_search_results.none");
|
||||
return i18n("discourse_ai.embeddings.semantic_search_results.none");
|
||||
}
|
||||
}
|
||||
|
||||
get settled() {
|
||||
return (
|
||||
this.validSearchOrder &&
|
||||
!this.searching &&
|
||||
this.searchTerm === this.initialSearchTerm
|
||||
);
|
||||
}
|
||||
|
||||
get noResults() {
|
||||
return this.settled && this.AiResults.length === 0;
|
||||
}
|
||||
|
||||
get searchTerm() {
|
||||
if (this.initialSearchTerm !== this.args.outletArgs.search) {
|
||||
if (this.initialSearchTerm !== this.args.searchTerm) {
|
||||
this.initialSearchTerm = undefined;
|
||||
}
|
||||
|
||||
return this.args.outletArgs.search;
|
||||
return this.args.searchTerm;
|
||||
}
|
||||
|
||||
get searchEnabled() {
|
||||
return (
|
||||
this.args.outletArgs.type === SEARCH_TYPE_DEFAULT &&
|
||||
this.args.searchType === SEARCH_TYPE_DEFAULT &&
|
||||
isValidSearchTerm(this.searchTerm, this.siteSettings) &&
|
||||
this.validSearchOrder
|
||||
);
|
||||
}
|
||||
|
||||
get searchClass() {
|
||||
if (!this.validSearchOrder) {
|
||||
return "unavailable";
|
||||
} else if (this.searching) {
|
||||
return "in-progress";
|
||||
} else if (this.noResults) {
|
||||
return "no-results";
|
||||
}
|
||||
}
|
||||
|
||||
get tooltipText() {
|
||||
return i18n(
|
||||
`discourse_ai.embeddings.semantic_search_tooltips.${
|
||||
this.validSearchOrder ? "results_explanation" : "invalid_sort"
|
||||
}`
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
toggleAiResults() {
|
||||
this.appEvents.trigger(AI_RESULTS_TOGGLED, {
|
||||
enabled: !this.showingAiResults,
|
||||
});
|
||||
if (this.showingAiResults) {
|
||||
this.args.outletArgs.addSearchResults([], "topic_id");
|
||||
this.args.addSearchResults([], "topic_id");
|
||||
} else {
|
||||
this.args.outletArgs.addSearchResults(this.AiResults, "topic_id");
|
||||
this.args.addSearchResults(this.AiResults, "topic_id");
|
||||
}
|
||||
this.showingAiResults = !this.showingAiResults;
|
||||
}
|
||||
@@ -139,7 +171,10 @@ export default class SemanticSearch extends Component {
|
||||
resetAiResults() {
|
||||
this.AiResults = [];
|
||||
this.showingAiResults = false;
|
||||
this.args.outletArgs.addSearchResults([], "topic_id");
|
||||
this.args.addSearchResults([], "topic_id");
|
||||
this.appEvents.trigger(AI_RESULTS_TOGGLED, {
|
||||
enabled: false,
|
||||
});
|
||||
}
|
||||
|
||||
performHyDESearch() {
|
||||
@@ -168,8 +203,8 @@ export default class SemanticSearch extends Component {
|
||||
|
||||
@action
|
||||
sortChanged() {
|
||||
if (this.sortOrder !== this.args.outletArgs.sortOrder) {
|
||||
this.sortOrder = this.args.outletArgs.sortOrder;
|
||||
if (this.sortOrder !== this.args.sortOrder) {
|
||||
this.sortOrder = this.args.sortOrder;
|
||||
|
||||
if (this.validSearchOrder) {
|
||||
this.onSearch();
|
||||
@@ -181,13 +216,11 @@ export default class SemanticSearch extends Component {
|
||||
}
|
||||
|
||||
<template>
|
||||
<span {{didUpdate this.sortChanged @outletArgs.sortOrder}}></span>
|
||||
<span {{didUpdate this.sortChanged @sortOrder}}></span>
|
||||
<div class="semantic-search__container search-results" role="region">
|
||||
<div class="semantic-search__results">
|
||||
<div
|
||||
class="semantic-search__searching
|
||||
{{if this.searching 'in-progress'}}
|
||||
{{unless this.validSearchOrder 'unavailable'}}"
|
||||
class={{concatClass "semantic-search__searching" this.searchClass}}
|
||||
>
|
||||
<DToggleSwitch
|
||||
disabled={{this.disableToggleSwitch}}
|
||||
@@ -205,22 +238,17 @@ export default class SemanticSearch extends Component {
|
||||
<AiIndicatorWave @loading={{this.searching}} />
|
||||
{{/if}}
|
||||
|
||||
{{#unless this.validSearchOrder}}
|
||||
|
||||
<DTooltip
|
||||
@identifier="semantic-search-unavailable-tooltip"
|
||||
class="semantic-search__unavailable-tooltip"
|
||||
>
|
||||
<:trigger>
|
||||
{{icon "far-circle-question"}}
|
||||
</:trigger>
|
||||
<:content>
|
||||
{{i18n
|
||||
"discourse_ai.embeddings.semantic_search_unavailable_tooltip"
|
||||
}}
|
||||
</:content>
|
||||
</DTooltip>
|
||||
{{/unless}}
|
||||
<DTooltip
|
||||
@identifier="semantic-search-tooltip"
|
||||
class="semantic-search__tooltip"
|
||||
>
|
||||
<:trigger>
|
||||
{{icon "far-circle-question"}}
|
||||
</:trigger>
|
||||
<:content>
|
||||
{{this.tooltipText}}
|
||||
</:content>
|
||||
</DTooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -12,11 +12,12 @@ import DButton from "discourse/components/d-button";
|
||||
import Avatar from "discourse/helpers/bound-avatar-template";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import icon from "discourse-common/helpers/d-icon";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
import I18n from "discourse-i18n";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import AdminUser from "admin/models/admin-user";
|
||||
import ComboBox from "select-kit/components/combo-box";
|
||||
import DTooltip from "float-kit/components/d-tooltip";
|
||||
import AiLlmQuotaEditor from "./ai-llm-quota-editor";
|
||||
import AiLlmQuotaModal from "./modal/ai-llm-quota-modal";
|
||||
|
||||
export default class AiLlmEditorForm extends Component {
|
||||
@service toasts;
|
||||
@@ -29,10 +30,18 @@ export default class AiLlmEditorForm extends Component {
|
||||
@tracked testResult = null;
|
||||
@tracked testError = null;
|
||||
@tracked apiKeySecret = true;
|
||||
@tracked quotaCount = 0;
|
||||
|
||||
@tracked modalIsVisible = false;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.updateQuotaCount();
|
||||
}
|
||||
|
||||
get selectedProviders() {
|
||||
const t = (provName) => {
|
||||
return I18n.t(`discourse_ai.llms.providers.${provName}`);
|
||||
return i18n(`discourse_ai.llms.providers.${provName}`);
|
||||
};
|
||||
|
||||
return this.args.llms.resultSetMeta.providers.map((prov) => {
|
||||
@@ -45,7 +54,7 @@ export default class AiLlmEditorForm extends Component {
|
||||
}
|
||||
|
||||
get testErrorMessage() {
|
||||
return I18n.t("discourse_ai.llms.tests.failure", { error: this.testError });
|
||||
return i18n("discourse_ai.llms.tests.failure", { error: this.testError });
|
||||
}
|
||||
|
||||
get displayTestResult() {
|
||||
@@ -65,7 +74,7 @@ export default class AiLlmEditorForm extends Component {
|
||||
}
|
||||
|
||||
const localized = usedBy.map((m) => {
|
||||
return I18n.t(`discourse_ai.llms.usage.${m.type}`, {
|
||||
return i18n(`discourse_ai.llms.usage.${m.type}`, {
|
||||
persona: m.name,
|
||||
});
|
||||
});
|
||||
@@ -79,12 +88,30 @@ export default class AiLlmEditorForm extends Component {
|
||||
}
|
||||
|
||||
get inUseWarning() {
|
||||
return I18n.t("discourse_ai.llms.in_use_warning", {
|
||||
return i18n("discourse_ai.llms.in_use_warning", {
|
||||
settings: this.modulesUsingModel,
|
||||
count: this.args.model.used_by.length,
|
||||
});
|
||||
}
|
||||
|
||||
get showQuotas() {
|
||||
return this.quotaCount > 0;
|
||||
}
|
||||
|
||||
get showAddQuotaButton() {
|
||||
return !this.showQuotas && !this.args.model.isNew;
|
||||
}
|
||||
|
||||
@action
|
||||
updateQuotaCount() {
|
||||
this.quotaCount = this.args.model?.llm_quotas?.length || 0;
|
||||
}
|
||||
|
||||
@action
|
||||
openAddQuotaModal() {
|
||||
this.modalIsVisible = true;
|
||||
}
|
||||
|
||||
@computed("args.model.provider")
|
||||
get metaProviderParams() {
|
||||
return (
|
||||
@@ -106,7 +133,7 @@ export default class AiLlmEditorForm extends Component {
|
||||
this.router.transitionTo("adminPlugins.show.discourse-ai-llms.index");
|
||||
} else {
|
||||
this.toasts.success({
|
||||
data: { message: I18n.t("discourse_ai.llms.saved") },
|
||||
data: { message: i18n("discourse_ai.llms.saved") },
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
@@ -154,7 +181,7 @@ export default class AiLlmEditorForm extends Component {
|
||||
@action
|
||||
delete() {
|
||||
return this.dialog.confirm({
|
||||
message: I18n.t("discourse_ai.llms.confirm_delete"),
|
||||
message: i18n("discourse_ai.llms.confirm_delete"),
|
||||
didConfirm: () => {
|
||||
return this.args.model
|
||||
.destroyRecord()
|
||||
@@ -169,6 +196,12 @@ export default class AiLlmEditorForm extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
closeAddQuotaModal() {
|
||||
this.modalIsVisible = false;
|
||||
this.updateQuotaCount();
|
||||
}
|
||||
|
||||
<template>
|
||||
{{#if this.seeded}}
|
||||
<div class="alert alert-info">
|
||||
@@ -317,6 +350,17 @@ export default class AiLlmEditorForm extends Component {
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.showQuotas}}
|
||||
<div class="control-group">
|
||||
<label>{{i18n "discourse_ai.llms.quotas.title"}}</label>
|
||||
<AiLlmQuotaEditor
|
||||
@model={{@model}}
|
||||
@groups={{@groups}}
|
||||
@didUpdate={{this.updateQuotaCount}}
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="control-group ai-llm-editor__action_panel">
|
||||
<DButton
|
||||
class="ai-llm-editor__test"
|
||||
@@ -324,7 +368,19 @@ export default class AiLlmEditorForm extends Component {
|
||||
@disabled={{this.testRunning}}
|
||||
@label="discourse_ai.llms.tests.title"
|
||||
/>
|
||||
|
||||
{{#if this.showAddQuotaButton}}
|
||||
<DButton
|
||||
@action={{this.openAddQuotaModal}}
|
||||
@label="discourse_ai.llms.quotas.add"
|
||||
class="btn"
|
||||
/>
|
||||
{{#if this.modalIsVisible}}
|
||||
<AiLlmQuotaModal
|
||||
@model={{hash llm=@model}}
|
||||
@closeModal={{this.closeAddQuotaModal}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
<DButton
|
||||
class="btn-primary ai-llm-editor__save"
|
||||
@action={{this.save}}
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { fn, hash } 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 { i18n } from "discourse-i18n";
|
||||
import DurationSelector from "./ai-quota-duration-selector";
|
||||
import AiLlmQuotaModal from "./modal/ai-llm-quota-modal";
|
||||
|
||||
export default class AiLlmQuotaEditor extends Component {
|
||||
@service store;
|
||||
@service dialog;
|
||||
@service site;
|
||||
|
||||
@tracked newQuotaGroupIds = null;
|
||||
@tracked newQuotaTokens = null;
|
||||
@tracked newQuotaUsages = null;
|
||||
@tracked newQuotaDuration = 86400; // 1 day default
|
||||
@tracked modalIsVisible = false;
|
||||
|
||||
@action
|
||||
updateExistingQuotaTokens(quota, event) {
|
||||
quota.max_tokens = event.target.value;
|
||||
}
|
||||
|
||||
@action
|
||||
updateExistingQuotaUsages(quota, event) {
|
||||
quota.max_usages = event.target.value;
|
||||
}
|
||||
|
||||
@action
|
||||
updateExistingQuotaDuration(quota, value) {
|
||||
quota.duration_seconds = value;
|
||||
}
|
||||
|
||||
@action
|
||||
openAddQuotaModal() {
|
||||
this.modalIsVisible = true;
|
||||
}
|
||||
|
||||
get canAddQuota() {
|
||||
return (
|
||||
this.newQuotaGroupId &&
|
||||
(this.newQuotaTokens || this.newQuotaUsages) &&
|
||||
this.newQuotaDuration
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
updateQuotaTokens(event) {
|
||||
this.newQuotaTokens = event.target.value;
|
||||
}
|
||||
|
||||
@action
|
||||
updateQuotaUsages(event) {
|
||||
this.newQuotaUsages = event.target.value;
|
||||
}
|
||||
|
||||
@action
|
||||
updateQuotaDuration(event) {
|
||||
this.newQuotaDuration = event.target.value;
|
||||
}
|
||||
|
||||
@action
|
||||
updateGroups(groups) {
|
||||
this.newQuotaGroupIds = groups;
|
||||
}
|
||||
|
||||
@action
|
||||
async addQuota() {
|
||||
const quota = {
|
||||
group_id: this.newQuotaGroupIds[0],
|
||||
group_name: this.site.groups.findBy("id", this.newQuotaGroupIds[0])?.name,
|
||||
llm_model_id: this.args.model.id,
|
||||
max_tokens: this.newQuotaTokens,
|
||||
max_usages: this.newQuotaUsages,
|
||||
duration_seconds: this.newQuotaDuration,
|
||||
};
|
||||
this.args.model.llm_quotas.pushObject(quota);
|
||||
if (this.args.didUpdate) {
|
||||
this.args.didUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
async deleteQuota(quota) {
|
||||
this.args.model.llm_quotas.removeObject(quota);
|
||||
if (this.args.didUpdate) {
|
||||
this.args.didUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
closeAddQuotaModal() {
|
||||
this.modalIsVisible = false;
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="ai-llm-quotas">
|
||||
<table class="ai-llm-quotas__table">
|
||||
<thead class="ai-llm-quotas__table-head">
|
||||
<tr class="ai-llm-quotas__header-row">
|
||||
<th class="ai-llm-quotas__header">{{i18n
|
||||
"discourse_ai.llms.quotas.group"
|
||||
}}</th>
|
||||
<th class="ai-llm-quotas__header">{{i18n
|
||||
"discourse_ai.llms.quotas.max_tokens"
|
||||
}}</th>
|
||||
<th class="ai-llm-quotas__header">{{i18n
|
||||
"discourse_ai.llms.quotas.max_usages"
|
||||
}}</th>
|
||||
<th class="ai-llm-quotas__header">{{i18n
|
||||
"discourse_ai.llms.quotas.duration"
|
||||
}}</th>
|
||||
<th
|
||||
class="ai-llm-quotas__header ai-llm-quotas__header--actions"
|
||||
></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="ai-llm-quotas__table-body">
|
||||
{{#each @model.llm_quotas as |quota|}}
|
||||
<tr class="ai-llm-quotas__row">
|
||||
<td class="ai-llm-quotas__cell">{{quota.group_name}}</td>
|
||||
<td class="ai-llm-quotas__cell">
|
||||
<input
|
||||
type="number"
|
||||
value={{quota.max_tokens}}
|
||||
class="ai-llm-quotas__input"
|
||||
min="1"
|
||||
{{on "input" (fn this.updateExistingQuotaTokens quota)}}
|
||||
/>
|
||||
</td>
|
||||
<td class="ai-llm-quotas__cell">
|
||||
<input
|
||||
type="number"
|
||||
value={{quota.max_usages}}
|
||||
class="ai-llm-quotas__input"
|
||||
min="1"
|
||||
{{on "input" (fn this.updateExistingQuotaUsages quota)}}
|
||||
/>
|
||||
</td>
|
||||
<td class="ai-llm-quotas__cell">
|
||||
<DurationSelector
|
||||
@value={{quota.duration_seconds}}
|
||||
@onChange={{fn this.updateExistingQuotaDuration quota}}
|
||||
/>
|
||||
</td>
|
||||
<td class="ai-llm-quotas__cell ai-llm-quotas__cell--actions">
|
||||
<DButton
|
||||
@icon="trash-alt"
|
||||
class="btn-danger ai-llm-quotas__delete-btn"
|
||||
@action={{fn this.deleteQuota quota}}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="ai-llm-quotas__actions">
|
||||
<DButton
|
||||
@action={{this.openAddQuotaModal}}
|
||||
@icon="plus"
|
||||
@label="discourse_ai.llms.quotas.add"
|
||||
class="btn"
|
||||
/>
|
||||
|
||||
{{#if this.modalIsVisible}}
|
||||
<AiLlmQuotaModal
|
||||
@model={{hash llm=@model}}
|
||||
@closeModal={{this.closeAddQuotaModal}}
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { computed } from "@ember/object";
|
||||
import { observes } from "@ember-decorators/object";
|
||||
import I18n from "discourse-i18n";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import ComboBox from "select-kit/components/combo-box";
|
||||
import { selectKitOptions } from "select-kit/components/select-kit";
|
||||
|
||||
@@ -18,7 +18,7 @@ export default class AiLlmSelector extends ComboBox {
|
||||
return [
|
||||
{
|
||||
id: "blank",
|
||||
name: I18n.t("discourse_ai.ai_persona.no_llm_selected"),
|
||||
name: i18n("discourse_ai.ai_persona.no_llm_selected"),
|
||||
},
|
||||
].concat(this.llms);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { concat, fn } 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 i18n from "discourse-common/helpers/i18n";
|
||||
import I18n from "discourse-i18n";
|
||||
import I18n, { i18n } from "discourse-i18n";
|
||||
import AdminSectionLandingItem from "admin/components/admin-section-landing-item";
|
||||
import AdminSectionLandingWrapper from "admin/components/admin-section-landing-wrapper";
|
||||
import DTooltip from "float-kit/components/d-tooltip";
|
||||
import AiLlmEditor from "./ai-llm-editor";
|
||||
|
||||
function isPreseeded(llm) {
|
||||
if (llm.id < 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export default class AiLlmsListEditor extends Component {
|
||||
@service adminPluginNavManager;
|
||||
@service router;
|
||||
@@ -31,11 +37,20 @@ export default class AiLlmsListEditor extends Component {
|
||||
|
||||
key = `discourse_ai.llms.model_description.${key}`;
|
||||
if (I18n.lookup(key, { ignoreMissing: true })) {
|
||||
return I18n.t(key);
|
||||
return i18n(key);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
@action
|
||||
preseededDescription(llm) {
|
||||
if (isPreseeded(llm)) {
|
||||
return i18n("discourse_ai.llms.preseeded_model_description", {
|
||||
model: llm.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
sanitizedTranslationKey(id) {
|
||||
return id.replace(/\./g, "-");
|
||||
}
|
||||
@@ -56,7 +71,7 @@ export default class AiLlmsListEditor extends Component {
|
||||
const options = [
|
||||
{
|
||||
id: "none",
|
||||
name: I18n.t("discourse_ai.llms.preconfigured.fake"),
|
||||
name: i18n("discourse_ai.llms.preconfigured.fake"),
|
||||
provider: "fake",
|
||||
},
|
||||
];
|
||||
@@ -98,11 +113,11 @@ export default class AiLlmsListEditor extends Component {
|
||||
|
||||
localizeUsage(usage) {
|
||||
if (usage.type === "ai_persona") {
|
||||
return I18n.t("discourse_ai.llms.usage.ai_persona", {
|
||||
return i18n("discourse_ai.llms.usage.ai_persona", {
|
||||
persona: usage.name,
|
||||
});
|
||||
} else {
|
||||
return I18n.t("discourse_ai.llms.usage." + usage.type);
|
||||
return i18n("discourse_ai.llms.usage." + usage.type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,14 +134,17 @@ export default class AiLlmsListEditor extends Component {
|
||||
@llmTemplate={{@llmTemplate}}
|
||||
/>
|
||||
{{else}}
|
||||
<DPageSubheader
|
||||
@titleLabel={{i18n "discourse_ai.llms.short_title"}}
|
||||
@descriptionLabel={{i18n
|
||||
"discourse_ai.llms.preconfigured.description"
|
||||
}}
|
||||
@learnMoreUrl="https://meta.discourse.org/t/discourse-ai-large-language-model-llm-settings-page/319903"
|
||||
/>
|
||||
{{#if this.hasLlmElements}}
|
||||
<section class="ai-llms-list-editor__configured">
|
||||
<DPageSubheader
|
||||
@titleLabel={{i18n "discourse_ai.llms.configured.title"}}
|
||||
@descriptionLabel={{i18n
|
||||
"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">
|
||||
<thead>
|
||||
@@ -143,6 +161,7 @@ export default class AiLlmsListEditor extends Component {
|
||||
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}}
|
||||
@@ -150,6 +169,7 @@ export default class AiLlmsListEditor extends Component {
|
||||
</div>
|
||||
<div class="ai-llm-list__description">
|
||||
{{this.modelDescription llm}}
|
||||
{{this.preseededDescription llm}}
|
||||
</div>
|
||||
{{#if llm.used_by}}
|
||||
<ul class="ai-llm-list-editor__usages">
|
||||
@@ -168,15 +188,26 @@ export default class AiLlmsListEditor extends Component {
|
||||
}}
|
||||
</td>
|
||||
<td class="d-admin-row__controls">
|
||||
<LinkTo
|
||||
@route="adminPlugins.show.discourse-ai-llms.edit"
|
||||
class="btn btn-default btn-small ai-llm-list__edit-button"
|
||||
@model={{llm.id}}
|
||||
>
|
||||
<div class="d-button-label">
|
||||
{{i18n "discourse_ai.llms.edit"}}
|
||||
</div>
|
||||
</LinkTo>
|
||||
{{#if (isPreseeded llm)}}
|
||||
<DTooltip class="ai-llm-list__edit-disabled-tooltip">
|
||||
<:trigger>
|
||||
<DButton
|
||||
class="btn btn-default btn-small disabled"
|
||||
@label="discourse_ai.llms.edit"
|
||||
/>
|
||||
</:trigger>
|
||||
<:content>
|
||||
{{i18n "discourse_ai.llms.seeded_warning"}}
|
||||
</:content>
|
||||
</DTooltip>
|
||||
{{else}}
|
||||
<DButton
|
||||
class="btn btn-default btn-small ai-llm-list__delete-button"
|
||||
@label="discourse_ai.llms.edit"
|
||||
@route="adminPlugins.show.discourse-ai-llms.edit"
|
||||
@routeModels={{llm.id}}
|
||||
/>
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
@@ -185,18 +216,7 @@ export default class AiLlmsListEditor extends Component {
|
||||
</section>
|
||||
{{/if}}
|
||||
<section class="ai-llms-list-editor__templates">
|
||||
<DPageSubheader
|
||||
@titleLabel={{i18n this.preconfiguredTitle}}
|
||||
@descriptionLabel={{unless
|
||||
this.hasLlmElements
|
||||
(i18n "discourse_ai.llms.preconfigured.description")
|
||||
}}
|
||||
@learnMoreUrl={{unless
|
||||
this.hasLlmElements
|
||||
"https://meta.discourse.org/t/discourse-ai-large-language-model-llm-settings-page/319903"
|
||||
}}
|
||||
/>
|
||||
|
||||
<DPageSubheader @titleLabel={{i18n this.preconfiguredTitle}} />
|
||||
<AdminSectionLandingWrapper
|
||||
class="ai-llms-list-editor__templates-list"
|
||||
>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
import Component from "@glimmer/component";
|
||||
import I18n from "discourse-i18n";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import AiPersonaToolOptionEditor from "./ai-persona-tool-option-editor";
|
||||
|
||||
export default class AiPersonaToolOptions extends Component {
|
||||
@@ -58,7 +58,7 @@ export default class AiPersonaToolOptions extends Component {
|
||||
<template>
|
||||
{{#if this.showToolOptions}}
|
||||
<div class="control-group">
|
||||
<label>{{I18n.t "discourse_ai.ai_persona.tool_options"}}</label>
|
||||
<label>{{i18n "discourse_ai.ai_persona.tool_options"}}</label>
|
||||
<div>
|
||||
{{#each this.toolOptions as |toolOption|}}
|
||||
<div class="ai-persona-editor__tool-options">
|
||||
|
||||
@@ -15,7 +15,7 @@ import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { sanitize } from "discourse/lib/text";
|
||||
import { clipboardCopy } from "discourse/lib/utilities";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
import I18n from "discourse-i18n";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import eq from "truth-helpers/helpers/eq";
|
||||
import AiHelperLoading from "../components/ai-helper-loading";
|
||||
import AiHelperOptionsList from "../components/ai-helper-options-list";
|
||||
@@ -55,7 +55,7 @@ export default class AiPostHelperMenu extends Component {
|
||||
|
||||
const instance = this.tooltip.register(element, {
|
||||
identifier: "cannot-add-footnote-tooltip",
|
||||
content: I18n.t(
|
||||
content: i18n(
|
||||
"discourse_ai.ai_helper.post_options_menu.footnote_disabled"
|
||||
),
|
||||
placement: "top",
|
||||
@@ -286,7 +286,7 @@ export default class AiPostHelperMenu extends Component {
|
||||
try {
|
||||
const result = await ajax(`/posts/${this.args.data.post.id}`);
|
||||
const sanitizedSuggestion = this._sanitizeForFootnote(this.suggestion);
|
||||
const credits = I18n.t(
|
||||
const credits = i18n(
|
||||
"discourse_ai.ai_helper.post_options_menu.footnote_credits"
|
||||
);
|
||||
const withFootnote = `${this.args.data.selectedText} ^[${sanitizedSuggestion} (${credits})]`;
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { on } from "@ember/modifier";
|
||||
import { action } from "@ember/object";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import ComboBox from "select-kit/components/combo-box";
|
||||
|
||||
const DURATION_PRESETS = [
|
||||
{
|
||||
id: "3600",
|
||||
seconds: 3600,
|
||||
nameKey: "discourse_ai.llms.quotas.durations.hour",
|
||||
},
|
||||
{
|
||||
id: "21600",
|
||||
seconds: 21600,
|
||||
nameKey: "discourse_ai.llms.quotas.durations.six_hours",
|
||||
},
|
||||
{
|
||||
id: "86400",
|
||||
seconds: 86400,
|
||||
nameKey: "discourse_ai.llms.quotas.durations.day",
|
||||
},
|
||||
{
|
||||
id: "604800",
|
||||
seconds: 604800,
|
||||
nameKey: "discourse_ai.llms.quotas.durations.week",
|
||||
},
|
||||
{ id: "custom", nameKey: "discourse_ai.llms.quotas.durations.custom" },
|
||||
];
|
||||
|
||||
export default class DurationSelector extends Component {
|
||||
@tracked selectedPresetId = "86400"; // Default to 1 day
|
||||
@tracked customHours = null;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
const seconds = this.args.value;
|
||||
const preset = DURATION_PRESETS.find((p) => p.seconds === seconds);
|
||||
if (preset) {
|
||||
this.selectedPresetId = preset.id;
|
||||
} else {
|
||||
this.selectedPresetId = "custom";
|
||||
this.customHours = Math.round(seconds / 3600);
|
||||
}
|
||||
}
|
||||
|
||||
get presetOptions() {
|
||||
return DURATION_PRESETS.map((preset) => ({
|
||||
id: preset.id,
|
||||
name: i18n(preset.nameKey),
|
||||
seconds: preset.seconds,
|
||||
}));
|
||||
}
|
||||
|
||||
get isCustom() {
|
||||
return this.selectedPresetId === "custom";
|
||||
}
|
||||
|
||||
get currentDurationSeconds() {
|
||||
if (this.isCustom) {
|
||||
return this.customHours ? this.customHours * 3600 : 0;
|
||||
} else {
|
||||
return parseInt(this.selectedPresetId, 10);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
onPresetChange(value) {
|
||||
this.selectedPresetId = value;
|
||||
this.updateValue();
|
||||
}
|
||||
|
||||
@action
|
||||
onCustomHoursChange(event) {
|
||||
this.customHours = parseInt(event.target.value, 10);
|
||||
this.updateValue();
|
||||
}
|
||||
|
||||
updateValue() {
|
||||
if (this.args.onChange) {
|
||||
this.args.onChange(this.currentDurationSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="duration-selector">
|
||||
<ComboBox
|
||||
@content={{this.presetOptions}}
|
||||
@value={{this.selectedPresetId}}
|
||||
@onChange={{this.onPresetChange}}
|
||||
class="duration-selector__preset"
|
||||
/>
|
||||
|
||||
{{#if this.isCustom}}
|
||||
<div class="duration-selector__custom">
|
||||
<input
|
||||
type="number"
|
||||
value={{this.customHours}}
|
||||
class="duration-selector__hours-input"
|
||||
min="1"
|
||||
{{on "input" this.onCustomHoursChange}}
|
||||
/>
|
||||
<span class="duration-selector__hours-label">
|
||||
{{i18n "discourse_ai.llms.quotas.hours"}}
|
||||
</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</template>
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import DTooltip from "discourse/components/d-tooltip";
|
||||
import withEventValue from "discourse/helpers/with-event-value";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import dIcon from "discourse-common/helpers/d-icon";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
import getURL from "discourse-common/lib/get-url";
|
||||
import AdminConfigAreaCard from "admin/components/admin-config-area-card";
|
||||
@@ -35,10 +36,51 @@ export default class AiSpam extends Component {
|
||||
@tracked isEnabled = false;
|
||||
@tracked selectedLLM = null;
|
||||
@tracked customInstructions = "";
|
||||
@tracked errors = [];
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.initializeFromModel();
|
||||
|
||||
if (this.args.model?.spam_scanning_user?.admin === false) {
|
||||
this.errors.push({
|
||||
message: i18n("discourse_ai.spam.errors.scan_not_admin.message"),
|
||||
button: {
|
||||
label: i18n("discourse_ai.spam.errors.scan_not_admin.action"),
|
||||
action: this.fixScanUserNotAdmin,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
async fixScanUserNotAdmin() {
|
||||
const spamScanningUser = this.args.model.spam_scanning_user;
|
||||
if (!spamScanningUser || spamScanningUser.admin) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await ajax(
|
||||
`/admin/plugins/discourse-ai/ai-spam/fix-errors`,
|
||||
{
|
||||
type: "POST",
|
||||
data: {
|
||||
error: "spam_scanner_not_admin",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (response.success) {
|
||||
this.toasts.success({
|
||||
data: { message: i18n("discourse_ai.spam.errors.resolved") },
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
popupAjaxError(error);
|
||||
} finally {
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
@@ -165,11 +207,22 @@ export default class AiSpam extends Component {
|
||||
<template>
|
||||
<div class="ai-spam">
|
||||
<section class="ai-spam__settings">
|
||||
<div class="ai-spam__errors">
|
||||
{{#each this.errors as |e|}}
|
||||
<div class="alert alert-error">
|
||||
{{dIcon "triangle-exclamation"}}
|
||||
<p>{{e.message}}</p>
|
||||
<DButton
|
||||
@action={{e.button.action}}
|
||||
@translatedLabel={{e.button.label}}
|
||||
/>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
<DPageSubheader
|
||||
@titleLabel={{i18n "discourse_ai.spam.title"}}
|
||||
@descriptionLabel={{i18n "discourse_ai.spam.spam_description"}}
|
||||
/>
|
||||
|
||||
<div class="control-group ai-spam__enabled">
|
||||
<DToggleSwitch
|
||||
class="ai-spam__toggle"
|
||||
|
||||
@@ -12,7 +12,7 @@ import DButton from "discourse/components/d-button";
|
||||
import DTooltip from "discourse/components/d-tooltip";
|
||||
import withEventValue from "discourse/helpers/with-event-value";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import I18n from "discourse-i18n";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import ComboBox from "select-kit/components/combo-box";
|
||||
import AiToolParameterEditor from "./ai-tool-parameter-editor";
|
||||
import AiToolTestModal from "./modal/ai-tool-test-modal";
|
||||
@@ -95,7 +95,7 @@ export default class AiToolEditor extends Component {
|
||||
await this.args.model.save(data);
|
||||
|
||||
this.toasts.success({
|
||||
data: { message: I18n.t("discourse_ai.tools.saved") },
|
||||
data: { message: i18n("discourse_ai.tools.saved") },
|
||||
duration: 2000,
|
||||
});
|
||||
if (!this.args.tools.any((tool) => tool.id === this.args.model.id)) {
|
||||
@@ -116,7 +116,7 @@ export default class AiToolEditor extends Component {
|
||||
@action
|
||||
delete() {
|
||||
return this.dialog.confirm({
|
||||
message: I18n.t("discourse_ai.tools.confirm_delete"),
|
||||
message: i18n("discourse_ai.tools.confirm_delete"),
|
||||
didConfirm: async () => {
|
||||
await this.args.model.destroyRecord();
|
||||
|
||||
@@ -148,7 +148,7 @@ export default class AiToolEditor extends Component {
|
||||
>
|
||||
{{#if this.showPresets}}
|
||||
<div class="control-group">
|
||||
<label>{{I18n.t "discourse_ai.tools.presets"}}</label>
|
||||
<label>{{i18n "discourse_ai.tools.presets"}}</label>
|
||||
<ComboBox
|
||||
@value={{this.presetId}}
|
||||
@content={{this.presets}}
|
||||
@@ -165,7 +165,7 @@ export default class AiToolEditor extends Component {
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="control-group">
|
||||
<label>{{I18n.t "discourse_ai.tools.name"}}</label>
|
||||
<label>{{i18n "discourse_ai.tools.name"}}</label>
|
||||
<input
|
||||
{{on "input" (withEventValue (fn (mut this.editingModel.name)))}}
|
||||
value={{this.editingModel.name}}
|
||||
@@ -174,24 +174,24 @@ export default class AiToolEditor extends Component {
|
||||
/>
|
||||
<DTooltip
|
||||
@icon="circle-question"
|
||||
@content={{I18n.t "discourse_ai.tools.name_help"}}
|
||||
@content={{i18n "discourse_ai.tools.name_help"}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>{{I18n.t "discourse_ai.tools.description"}}</label>
|
||||
<label>{{i18n "discourse_ai.tools.description"}}</label>
|
||||
<textarea
|
||||
{{on
|
||||
"input"
|
||||
(withEventValue (fn (mut this.editingModel.description)))
|
||||
}}
|
||||
placeholder={{I18n.t "discourse_ai.tools.description_help"}}
|
||||
placeholder={{i18n "discourse_ai.tools.description_help"}}
|
||||
class="ai-tool-editor__description input-xxlarge"
|
||||
>{{this.editingModel.description}}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>{{I18n.t "discourse_ai.tools.summary"}}</label>
|
||||
<label>{{i18n "discourse_ai.tools.summary"}}</label>
|
||||
<input
|
||||
{{on "input" (withEventValue (fn (mut this.editingModel.summary)))}}
|
||||
value={{this.editingModel.summary}}
|
||||
@@ -200,17 +200,17 @@ export default class AiToolEditor extends Component {
|
||||
/>
|
||||
<DTooltip
|
||||
@icon="circle-question"
|
||||
@content={{I18n.t "discourse_ai.tools.summary_help"}}
|
||||
@content={{i18n "discourse_ai.tools.summary_help"}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>{{I18n.t "discourse_ai.tools.parameters"}}</label>
|
||||
<label>{{i18n "discourse_ai.tools.parameters"}}</label>
|
||||
<AiToolParameterEditor @parameters={{this.editingModel.parameters}} />
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>{{I18n.t "discourse_ai.tools.script"}}</label>
|
||||
<label>{{i18n "discourse_ai.tools.script"}}</label>
|
||||
<AceEditor
|
||||
@content={{this.editingModel.script}}
|
||||
@onChange={{fn (mut this.editingModel.script)}}
|
||||
|
||||
@@ -3,8 +3,7 @@ import { LinkTo } from "@ember/routing";
|
||||
import { service } from "@ember/service";
|
||||
import DBreadcrumbsItem from "discourse/components/d-breadcrumbs-item";
|
||||
import DPageSubheader from "discourse/components/d-page-subheader";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
import I18n from "discourse-i18n";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import AdminConfigAreaEmptyList from "admin/components/admin-config-area-empty-list";
|
||||
|
||||
export default class AiToolListEditor extends Component {
|
||||
@@ -60,7 +59,7 @@ export default class AiToolListEditor extends Component {
|
||||
@route="adminPlugins.show.discourse-ai-tools.edit"
|
||||
@model={{tool}}
|
||||
class="btn btn-text btn-small"
|
||||
>{{I18n.t "discourse_ai.tools.edit"}}</LinkTo>
|
||||
>{{i18n "discourse_ai.tools.edit"}}</LinkTo>
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { action } from "@ember/object";
|
||||
import { TrackedArray, TrackedObject } from "@ember-compat/tracked-built-ins";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import withEventValue from "discourse/helpers/with-event-value";
|
||||
import I18n from "discourse-i18n";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import ComboBox from "select-kit/components/combo-box";
|
||||
|
||||
const PARAMETER_TYPES = [
|
||||
@@ -76,7 +76,7 @@ export default class AiToolParameterEditor extends Component {
|
||||
{{on "input" (withEventValue (fn (mut parameter.name)))}}
|
||||
value={{parameter.name}}
|
||||
type="text"
|
||||
placeholder={{I18n.t "discourse_ai.tools.parameter_name"}}
|
||||
placeholder={{i18n "discourse_ai.tools.parameter_name"}}
|
||||
/>
|
||||
<ComboBox @value={{parameter.type}} @content={{PARAMETER_TYPES}} />
|
||||
</div>
|
||||
@@ -86,7 +86,7 @@ export default class AiToolParameterEditor extends Component {
|
||||
{{on "input" (withEventValue (fn (mut parameter.description)))}}
|
||||
value={{parameter.description}}
|
||||
type="text"
|
||||
placeholder={{I18n.t "discourse_ai.tools.parameter_description"}}
|
||||
placeholder={{i18n "discourse_ai.tools.parameter_description"}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -98,7 +98,7 @@ export default class AiToolParameterEditor extends Component {
|
||||
type="checkbox"
|
||||
class="parameter-row__required-toggle"
|
||||
/>
|
||||
{{I18n.t "discourse_ai.tools.parameter_required"}}
|
||||
{{i18n "discourse_ai.tools.parameter_required"}}
|
||||
</label>
|
||||
|
||||
<label>
|
||||
@@ -108,7 +108,7 @@ export default class AiToolParameterEditor extends Component {
|
||||
type="checkbox"
|
||||
class="parameter-row__enum-toggle"
|
||||
/>
|
||||
{{I18n.t "discourse_ai.tools.parameter_enum"}}
|
||||
{{i18n "discourse_ai.tools.parameter_enum"}}
|
||||
</label>
|
||||
|
||||
<DButton
|
||||
@@ -126,7 +126,7 @@ export default class AiToolParameterEditor extends Component {
|
||||
{{on "change" (fn this.updateEnumValue parameter enumIndex)}}
|
||||
value={{enumValue}}
|
||||
type="text"
|
||||
placeholder={{I18n.t "discourse_ai.tools.enum_value"}}
|
||||
placeholder={{i18n "discourse_ai.tools.enum_value"}}
|
||||
/>
|
||||
<DButton
|
||||
@action={{fn this.removeEnumValue parameter enumIndex}}
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { hash } from "@ember/helper";
|
||||
import { on } from "@ember/modifier";
|
||||
import { action } from "@ember/object";
|
||||
import { service } from "@ember/service";
|
||||
import { not } from "truth-helpers";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import DModal from "discourse/components/d-modal";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import GroupChooser from "select-kit/components/group-chooser";
|
||||
import DTooltip from "float-kit/components/d-tooltip";
|
||||
import DurationSelector from "../ai-quota-duration-selector";
|
||||
|
||||
export default class AiLlmQuotaModal extends Component {
|
||||
@service site;
|
||||
|
||||
@tracked groupIds = null;
|
||||
@tracked maxTokens = null;
|
||||
@tracked maxUsages = null;
|
||||
@tracked duration = 86400; // Default 1 day
|
||||
|
||||
get canSave() {
|
||||
return (
|
||||
this.groupIds?.length > 0 &&
|
||||
(this.maxTokens || this.maxUsages) &&
|
||||
this.duration
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
updateGroups(groups) {
|
||||
this.groupIds = groups;
|
||||
}
|
||||
|
||||
@action
|
||||
updateDuration(value) {
|
||||
this.duration = value;
|
||||
}
|
||||
|
||||
@action
|
||||
updateMaxTokens(event) {
|
||||
this.maxTokens = event.target.value;
|
||||
}
|
||||
|
||||
@action
|
||||
updateMaxUsages(event) {
|
||||
this.maxUsages = event.target.value;
|
||||
}
|
||||
|
||||
@action
|
||||
save() {
|
||||
const quota = {
|
||||
group_id: this.groupIds[0],
|
||||
group_name: this.site.groups.findBy("id", this.groupIds[0]).name,
|
||||
llm_model_id: this.args.model.id,
|
||||
max_tokens: this.maxTokens,
|
||||
max_usages: this.maxUsages,
|
||||
duration_seconds: this.duration,
|
||||
};
|
||||
|
||||
this.args.model.llm.llm_quotas.pushObject(quota);
|
||||
this.args.closeModal();
|
||||
if (this.args.model.onSave) {
|
||||
this.args.model.onSave();
|
||||
}
|
||||
}
|
||||
|
||||
get availableGroups() {
|
||||
const existingQuotaGroupIds =
|
||||
this.args.model.llm.llm_quotas.map((q) => q.group_id) || [];
|
||||
|
||||
return this.site.groups.filter(
|
||||
(group) => !existingQuotaGroupIds.includes(group.id) && group.id !== 0
|
||||
);
|
||||
}
|
||||
|
||||
<template>
|
||||
<DModal
|
||||
@title={{i18n "discourse_ai.llms.quotas.add_title"}}
|
||||
@closeModal={{@closeModal}}
|
||||
class="ai-llm-quota-modal"
|
||||
>
|
||||
<:body>
|
||||
<div class="control-group">
|
||||
<label>{{i18n "discourse_ai.llms.quotas.group"}}</label>
|
||||
<GroupChooser
|
||||
@value={{this.groupIds}}
|
||||
@content={{this.availableGroups}}
|
||||
@onChange={{this.updateGroups}}
|
||||
@options={{hash maximum=1}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>{{i18n "discourse_ai.llms.quotas.max_tokens"}}</label>
|
||||
<input
|
||||
type="number"
|
||||
value={{this.maxTokens}}
|
||||
class="input-large"
|
||||
min="1"
|
||||
{{on "input" this.updateMaxTokens}}
|
||||
/>
|
||||
<DTooltip
|
||||
@icon="circle-question"
|
||||
@content={{i18n "discourse_ai.llms.quotas.max_tokens_help"}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>{{i18n "discourse_ai.llms.quotas.max_usages"}}</label>
|
||||
<input
|
||||
type="number"
|
||||
value={{this.maxUsages}}
|
||||
class="input-large"
|
||||
min="1"
|
||||
{{on "input" this.updateMaxUsages}}
|
||||
/>
|
||||
<DTooltip
|
||||
@icon="circle-question"
|
||||
@content={{i18n "discourse_ai.llms.quotas.max_usages_help"}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>{{i18n "discourse_ai.llms.quotas.duration"}}</label>
|
||||
<DurationSelector
|
||||
@value={{this.duration}}
|
||||
@onChange={{this.updateDuration}}
|
||||
/>
|
||||
</div>
|
||||
</:body>
|
||||
|
||||
<:footer>
|
||||
<DButton
|
||||
@action={{this.save}}
|
||||
@label="discourse_ai.llms.quotas.add"
|
||||
@disabled={{not this.canSave}}
|
||||
class="btn-primary"
|
||||
/>
|
||||
</:footer>
|
||||
</DModal>
|
||||
</template>
|
||||
}
|
||||
@@ -16,9 +16,8 @@ import htmlClass from "discourse/helpers/html-class";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { shortDateNoYear } from "discourse/lib/formatter";
|
||||
import dIcon from "discourse-common/helpers/d-icon";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
import I18n from "discourse-i18n";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import DTooltip from "float-kit/components/d-tooltip";
|
||||
import AiSummarySkeleton from "../../components/ai-summary-skeleton";
|
||||
|
||||
@@ -45,11 +44,11 @@ export default class AiSummaryModal extends Component {
|
||||
streamedTextLength = 0;
|
||||
|
||||
get outdatedSummaryWarningText() {
|
||||
let outdatedText = I18n.t("summary.outdated");
|
||||
let outdatedText = i18n("summary.outdated");
|
||||
|
||||
if (!this.topRepliesSummaryEnabled && this.newPostsSinceSummary > 0) {
|
||||
outdatedText += " ";
|
||||
outdatedText += I18n.t("summary.outdated_posts", {
|
||||
outdatedText += i18n("summary.outdated_posts", {
|
||||
count: this.newPostsSinceSummary,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import DButton from "discourse/components/d-button";
|
||||
import DModal from "discourse/components/d-modal";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import I18n from "discourse-i18n";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import { jsonToHtml } from "../../lib/utilities";
|
||||
|
||||
export default class AiToolTestModal extends Component {
|
||||
@@ -45,7 +45,7 @@ export default class AiToolTestModal extends Component {
|
||||
|
||||
<template>
|
||||
<DModal
|
||||
@title={{I18n.t "discourse_ai.tools.test_modal.title"}}
|
||||
@title={{i18n "discourse_ai.tools.test_modal.title"}}
|
||||
@closeModal={{@closeModal}}
|
||||
@bodyClass="ai-tool-test-modal__body"
|
||||
class="ai-tool-test-modal"
|
||||
@@ -64,7 +64,7 @@ export default class AiToolTestModal extends Component {
|
||||
|
||||
{{#if this.testResult}}
|
||||
<div class="ai-tool-test-modal__test-result">
|
||||
<h3>{{I18n.t "discourse_ai.tools.test_modal.result"}}</h3>
|
||||
<h3>{{i18n "discourse_ai.tools.test_modal.result"}}</h3>
|
||||
<div>{{this.testResult}}</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user