Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
522c1aabb8 |
@@ -1,4 +1,3 @@
|
||||
< 3.4.0.beta4-dev: 20612fde52d3f740cad64823ef8aadb0748b567f
|
||||
< 3.4.0.beta3-dev: decf1bb49d737ea15308400f22f89d1d1e71d13d
|
||||
< 3.4.0.beta1-dev: 9d887ad4ace8e33c3fe7dbb39237e882c08b4f0b
|
||||
< 3.3.0.beta5-dev: 4d8090002f6dcd8e34d41033606bf131fa221475
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
# 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.includes(:llm_quotas).order(:display_name)
|
||||
llms = LlmModel.all.order(:display_name)
|
||||
|
||||
render json: {
|
||||
ai_llms:
|
||||
@@ -40,11 +40,6 @@ 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
|
||||
@@ -56,25 +51,6 @@ 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
|
||||
@@ -134,19 +110,6 @@ 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?(
|
||||
llm_model_id.to_s,
|
||||
"custom:#{llm_model_id}",
|
||||
)
|
||||
return(
|
||||
render_json_error(
|
||||
@@ -86,31 +86,6 @@ 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,7 +57,6 @@ 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,7 +4,6 @@ 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 }
|
||||
@@ -33,7 +32,6 @@ class LlmModel < ActiveRecord::Base
|
||||
open_ai: {
|
||||
organization: :text,
|
||||
disable_native_tools: :checkbox,
|
||||
disable_streaming: :checkbox,
|
||||
},
|
||||
mistral: {
|
||||
disable_native_tools: :checkbox,
|
||||
@@ -53,13 +51,11 @@ 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
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
# 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)
|
||||
#
|
||||
@@ -1,120 +0,0 @@
|
||||
# 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,8 +7,7 @@ class AiSpamSerializer < ApplicationSerializer
|
||||
:available_llms,
|
||||
:stats,
|
||||
:flagging_username,
|
||||
:spam_score_type,
|
||||
:spam_scanning_user
|
||||
:spam_score_type
|
||||
|
||||
def is_enabled
|
||||
object[:enabled]
|
||||
@@ -48,10 +47,4 @@ 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,7 +20,6 @@ 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 =
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
# 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(
|
||||
const translatedName = I18n.t(
|
||||
"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("discourse_ai.ai_helper.no_content_error"),
|
||||
message: I18n.t("discourse_ai.ai_helper.no_content_error"),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { on } from "@ember/modifier";
|
||||
import { action } from "@ember/object";
|
||||
import { service } from "@ember/service";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { getOwner } from "discourse-common/lib/get-owner";
|
||||
|
||||
const FILTERS = {
|
||||
category: "category:",
|
||||
tags: "tags:",
|
||||
"created by": "created-by:",
|
||||
"status archived": "status:archived",
|
||||
"status closed": "status:closed",
|
||||
"status deleted": "status:deleted",
|
||||
"status listed": "status:listed",
|
||||
"status open": "status:open",
|
||||
"status public": "status:public",
|
||||
"status unlisted": "status:unlisted",
|
||||
"in bookmarked": "in:bookmarked",
|
||||
"in muted": "in:muted",
|
||||
"in normal": "in:normal",
|
||||
"in pinned": "in:pinned",
|
||||
"in tracking": "in:tracking",
|
||||
"in watching": "in:watching",
|
||||
"activity before": "activity-before:",
|
||||
"activity after": "activity-after:",
|
||||
"created before": "created-before:",
|
||||
"created after": "created-after:",
|
||||
"latest post before": "latest-post-before:",
|
||||
"latest post after": "latest-post-after:",
|
||||
"minimum likes": "likes-min:",
|
||||
"maximum likes": "likes-max:",
|
||||
"minimum likes by original poster": "likes-op-min:",
|
||||
"maximum likes by original poster": "likes-op-max:",
|
||||
"minimum posts": "posts-min:",
|
||||
"maximum posts": "posts-max:",
|
||||
"minimum posters": "posters-min:",
|
||||
"maximum posters": "posters-max:",
|
||||
"minimum views": "views-min:",
|
||||
"maximum views": "views-max:",
|
||||
"sort by activity": "order:activity",
|
||||
"sort by category": "order:category",
|
||||
"sort by created date": "order:created",
|
||||
"sort by latest post": "order:latest-post",
|
||||
"sort by likes": "order:likes",
|
||||
"sort by likes (original poster)": "order:likes-op",
|
||||
"sort by posters": "order:posters",
|
||||
"sort by views": "order:views",
|
||||
};
|
||||
|
||||
function constructQueryString(aiFilters) {
|
||||
return aiFilters
|
||||
.flatMap((filterLine) => {
|
||||
// Split on spaces to get individual "key:value" segments
|
||||
return filterLine.split(" ").filter((segment) => segment.includes(":"));
|
||||
})
|
||||
.map((filterSegment) => {
|
||||
const [key, ...valueParts] = filterSegment.split(":");
|
||||
const value = valueParts.join(":").trim();
|
||||
|
||||
const validKey = Object.keys(FILTERS).find(
|
||||
(filterKey) => filterKey.toLowerCase() === key.toLowerCase()
|
||||
);
|
||||
|
||||
if (validKey) {
|
||||
return `${FILTERS[validKey]}${value}`;
|
||||
}
|
||||
|
||||
// If the segment already starts with a known prefix
|
||||
if (
|
||||
Object.values(FILTERS).some((filterValue) =>
|
||||
filterSegment.startsWith(filterValue)
|
||||
)
|
||||
) {
|
||||
return filterSegment.trim();
|
||||
}
|
||||
|
||||
// Otherwise, it's invalid
|
||||
return null;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
export default class AiFilterInput extends Component {
|
||||
@service site;
|
||||
|
||||
@tracked userInput = "";
|
||||
@tracked loading = false;
|
||||
@tracked error = null;
|
||||
|
||||
siteCategories = [
|
||||
"gaming",
|
||||
"support",
|
||||
"general",
|
||||
"announcements",
|
||||
"off-topic",
|
||||
];
|
||||
|
||||
get discoveryFilter() {
|
||||
return getOwner(this).lookup("controller:discovery/filter");
|
||||
}
|
||||
|
||||
@action
|
||||
handleInputChange(event) {
|
||||
this.userInput = event.target.value;
|
||||
}
|
||||
|
||||
@action
|
||||
handleKeydown(event) {
|
||||
if (event.key === "Enter") {
|
||||
this.submitQuery();
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
async submitQuery() {
|
||||
if (!this.userInput.trim()) {
|
||||
this.error = "Please enter a query.";
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
|
||||
try {
|
||||
const categoriesListStr = this.site.categories
|
||||
.map((cat) => cat.name)
|
||||
.join(", ");
|
||||
const now = new Date();
|
||||
const currentYear = now.getFullYear();
|
||||
const currentMonth = String(now.getMonth() + 1).padStart(2, "0");
|
||||
const currentDay = String(now.getDate()).padStart(2, "0");
|
||||
const currentDateStr = `${currentYear}-${currentMonth}-${currentDay}`;
|
||||
|
||||
const response = await ajax("/discourse-ai/ai-helper/suggest", {
|
||||
method: "POST",
|
||||
data: {
|
||||
text: this.userInput,
|
||||
mode: -305,
|
||||
custom_prompt: `
|
||||
The following are valid categories on this site: ${categoriesListStr}.
|
||||
The current date (in the user's time) is: ${currentDateStr}.
|
||||
|
||||
If the user references dates, convert them to YYYY-MM-DD.
|
||||
If the user says "0 replies," that means posts-max:1.
|
||||
Map ambiguous categories to the closest match.
|
||||
|
||||
Return filters as a clean, space-separated string without quotes.
|
||||
Do not add extra quotes around filters.
|
||||
`,
|
||||
},
|
||||
});
|
||||
|
||||
let aiSuggestions = response.suggestions || "";
|
||||
aiSuggestions = aiSuggestions.replace(/"/g, ""); // remove all quotes
|
||||
|
||||
// turn it into an array with one element
|
||||
const aiFilters = aiSuggestions ? [aiSuggestions] : [];
|
||||
|
||||
let queryString = constructQueryString(aiFilters);
|
||||
|
||||
// remove quotes
|
||||
queryString = queryString.replace(/"/g, "");
|
||||
|
||||
this.args.updateQueryString(queryString);
|
||||
this.discoveryFilter.updateTopicsListQueryParams(queryString);
|
||||
|
||||
this.aiTitle = aiTitle;
|
||||
this.aiExplanation = aiExplanation;
|
||||
|
||||
document.getElementById("queryStringInput").focus();
|
||||
} catch (error) {
|
||||
popupAjaxError(error);
|
||||
this.error = "Failed to process the query. Please try again.";
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="ai-filter-input">
|
||||
{{#if this.aiTitle}}
|
||||
<h2>{{this.aiTitle}}</h2>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.aiExplanation}}
|
||||
<p class="explanation">{{this.aiExplanation}}</p>
|
||||
{{/if}}
|
||||
|
||||
<input
|
||||
type="text"
|
||||
id="aiQueryStringInput"
|
||||
placeholder="Enter your query..."
|
||||
value={{this.userInput}}
|
||||
{{on "input" this.handleInputChange}}
|
||||
{{on "keydown" this.handleKeydown}}
|
||||
/>
|
||||
|
||||
{{if this.loading "Processing..."}}
|
||||
|
||||
{{#if this.error}}
|
||||
<p class="error">{{this.error}}</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
</template>
|
||||
}
|
||||
@@ -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("discourse_ai.ai_persona.tool_strategies.all"),
|
||||
name: I18n.t("discourse_ai.ai_persona.tool_strategies.all"),
|
||||
},
|
||||
];
|
||||
|
||||
[1, 2, 5].forEach((i) => {
|
||||
content.push({
|
||||
id: i,
|
||||
name: i18n("discourse_ai.ai_persona.tool_strategies.replies", {
|
||||
name: I18n.t("discourse_ai.ai_persona.tool_strategies.replies", {
|
||||
count: i,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -12,12 +12,11 @@ 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-i18n";
|
||||
import i18n from "discourse-common/helpers/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;
|
||||
@@ -30,18 +29,10 @@ 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(`discourse_ai.llms.providers.${provName}`);
|
||||
return I18n.t(`discourse_ai.llms.providers.${provName}`);
|
||||
};
|
||||
|
||||
return this.args.llms.resultSetMeta.providers.map((prov) => {
|
||||
@@ -54,7 +45,7 @@ export default class AiLlmEditorForm extends Component {
|
||||
}
|
||||
|
||||
get testErrorMessage() {
|
||||
return i18n("discourse_ai.llms.tests.failure", { error: this.testError });
|
||||
return I18n.t("discourse_ai.llms.tests.failure", { error: this.testError });
|
||||
}
|
||||
|
||||
get displayTestResult() {
|
||||
@@ -74,7 +65,7 @@ export default class AiLlmEditorForm extends Component {
|
||||
}
|
||||
|
||||
const localized = usedBy.map((m) => {
|
||||
return i18n(`discourse_ai.llms.usage.${m.type}`, {
|
||||
return I18n.t(`discourse_ai.llms.usage.${m.type}`, {
|
||||
persona: m.name,
|
||||
});
|
||||
});
|
||||
@@ -88,30 +79,12 @@ export default class AiLlmEditorForm extends Component {
|
||||
}
|
||||
|
||||
get inUseWarning() {
|
||||
return i18n("discourse_ai.llms.in_use_warning", {
|
||||
return I18n.t("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 (
|
||||
@@ -133,7 +106,7 @@ export default class AiLlmEditorForm extends Component {
|
||||
this.router.transitionTo("adminPlugins.show.discourse-ai-llms.index");
|
||||
} else {
|
||||
this.toasts.success({
|
||||
data: { message: i18n("discourse_ai.llms.saved") },
|
||||
data: { message: I18n.t("discourse_ai.llms.saved") },
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
@@ -181,7 +154,7 @@ export default class AiLlmEditorForm extends Component {
|
||||
@action
|
||||
delete() {
|
||||
return this.dialog.confirm({
|
||||
message: i18n("discourse_ai.llms.confirm_delete"),
|
||||
message: I18n.t("discourse_ai.llms.confirm_delete"),
|
||||
didConfirm: () => {
|
||||
return this.args.model
|
||||
.destroyRecord()
|
||||
@@ -196,12 +169,6 @@ export default class AiLlmEditorForm extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
closeAddQuotaModal() {
|
||||
this.modalIsVisible = false;
|
||||
this.updateQuotaCount();
|
||||
}
|
||||
|
||||
<template>
|
||||
{{#if this.seeded}}
|
||||
<div class="alert alert-info">
|
||||
@@ -350,17 +317,6 @@ 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"
|
||||
@@ -368,19 +324,7 @@ 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}}
|
||||
|
||||
@@ -1,178 +0,0 @@
|
||||
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("discourse_ai.ai_persona.no_llm_selected"),
|
||||
name: I18n.t("discourse_ai.ai_persona.no_llm_selected"),
|
||||
},
|
||||
].concat(this.llms);
|
||||
}
|
||||
|
||||
@@ -1,22 +1,16 @@
|
||||
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, { i18n } from "discourse-i18n";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
import 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;
|
||||
@@ -37,20 +31,11 @@ export default class AiLlmsListEditor extends Component {
|
||||
|
||||
key = `discourse_ai.llms.model_description.${key}`;
|
||||
if (I18n.lookup(key, { ignoreMissing: true })) {
|
||||
return i18n(key);
|
||||
return I18n.t(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, "-");
|
||||
}
|
||||
@@ -71,7 +56,7 @@ export default class AiLlmsListEditor extends Component {
|
||||
const options = [
|
||||
{
|
||||
id: "none",
|
||||
name: i18n("discourse_ai.llms.preconfigured.fake"),
|
||||
name: I18n.t("discourse_ai.llms.preconfigured.fake"),
|
||||
provider: "fake",
|
||||
},
|
||||
];
|
||||
@@ -113,11 +98,11 @@ export default class AiLlmsListEditor extends Component {
|
||||
|
||||
localizeUsage(usage) {
|
||||
if (usage.type === "ai_persona") {
|
||||
return i18n("discourse_ai.llms.usage.ai_persona", {
|
||||
return I18n.t("discourse_ai.llms.usage.ai_persona", {
|
||||
persona: usage.name,
|
||||
});
|
||||
} else {
|
||||
return i18n("discourse_ai.llms.usage." + usage.type);
|
||||
return I18n.t("discourse_ai.llms.usage." + usage.type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,17 +119,14 @@ 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>
|
||||
@@ -161,7 +143,6 @@ 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}}
|
||||
@@ -169,7 +150,6 @@ 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">
|
||||
@@ -188,26 +168,15 @@ export default class AiLlmsListEditor extends Component {
|
||||
}}
|
||||
</td>
|
||||
<td class="d-admin-row__controls">
|
||||
{{#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}}
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
@@ -216,7 +185,18 @@ export default class AiLlmsListEditor extends Component {
|
||||
</section>
|
||||
{{/if}}
|
||||
<section class="ai-llms-list-editor__templates">
|
||||
<DPageSubheader @titleLabel={{i18n this.preconfiguredTitle}} />
|
||||
<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"
|
||||
}}
|
||||
/>
|
||||
|
||||
<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 "discourse_ai.ai_persona.tool_options"}}</label>
|
||||
<label>{{I18n.t "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(
|
||||
content: I18n.t(
|
||||
"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(
|
||||
const credits = I18n.t(
|
||||
"discourse_ai.ai_helper.post_options_menu.footnote_credits"
|
||||
);
|
||||
const withFootnote = `${this.args.data.selectedText} ^[${sanitizedSuggestion} (${credits})]`;
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
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,7 +13,6 @@ 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";
|
||||
@@ -36,51 +35,10 @@ 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
|
||||
@@ -207,22 +165,11 @@ 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("discourse_ai.tools.saved") },
|
||||
data: { message: I18n.t("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("discourse_ai.tools.confirm_delete"),
|
||||
message: I18n.t("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 "discourse_ai.tools.presets"}}</label>
|
||||
<label>{{I18n.t "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 "discourse_ai.tools.name"}}</label>
|
||||
<label>{{I18n.t "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 "discourse_ai.tools.name_help"}}
|
||||
@content={{I18n.t "discourse_ai.tools.name_help"}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>{{i18n "discourse_ai.tools.description"}}</label>
|
||||
<label>{{I18n.t "discourse_ai.tools.description"}}</label>
|
||||
<textarea
|
||||
{{on
|
||||
"input"
|
||||
(withEventValue (fn (mut this.editingModel.description)))
|
||||
}}
|
||||
placeholder={{i18n "discourse_ai.tools.description_help"}}
|
||||
placeholder={{I18n.t "discourse_ai.tools.description_help"}}
|
||||
class="ai-tool-editor__description input-xxlarge"
|
||||
>{{this.editingModel.description}}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>{{i18n "discourse_ai.tools.summary"}}</label>
|
||||
<label>{{I18n.t "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 "discourse_ai.tools.summary_help"}}
|
||||
@content={{I18n.t "discourse_ai.tools.summary_help"}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>{{i18n "discourse_ai.tools.parameters"}}</label>
|
||||
<label>{{I18n.t "discourse_ai.tools.parameters"}}</label>
|
||||
<AiToolParameterEditor @parameters={{this.editingModel.parameters}} />
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>{{i18n "discourse_ai.tools.script"}}</label>
|
||||
<label>{{I18n.t "discourse_ai.tools.script"}}</label>
|
||||
<AceEditor
|
||||
@content={{this.editingModel.script}}
|
||||
@onChange={{fn (mut this.editingModel.script)}}
|
||||
|
||||
@@ -3,7 +3,8 @@ 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-i18n";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
import I18n from "discourse-i18n";
|
||||
import AdminConfigAreaEmptyList from "admin/components/admin-config-area-empty-list";
|
||||
|
||||
export default class AiToolListEditor extends Component {
|
||||
@@ -59,7 +60,7 @@ export default class AiToolListEditor extends Component {
|
||||
@route="adminPlugins.show.discourse-ai-tools.edit"
|
||||
@model={{tool}}
|
||||
class="btn btn-text btn-small"
|
||||
>{{i18n "discourse_ai.tools.edit"}}</LinkTo>
|
||||
>{{I18n.t "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 "discourse_ai.tools.parameter_name"}}
|
||||
placeholder={{I18n.t "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 "discourse_ai.tools.parameter_description"}}
|
||||
placeholder={{I18n.t "discourse_ai.tools.parameter_description"}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -98,7 +98,7 @@ export default class AiToolParameterEditor extends Component {
|
||||
type="checkbox"
|
||||
class="parameter-row__required-toggle"
|
||||
/>
|
||||
{{i18n "discourse_ai.tools.parameter_required"}}
|
||||
{{I18n.t "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 "discourse_ai.tools.parameter_enum"}}
|
||||
{{I18n.t "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 "discourse_ai.tools.enum_value"}}
|
||||
placeholder={{I18n.t "discourse_ai.tools.enum_value"}}
|
||||
/>
|
||||
<DButton
|
||||
@action={{fn this.removeEnumValue parameter enumIndex}}
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
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,8 +16,9 @@ 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";
|
||||
|
||||
@@ -44,11 +45,11 @@ export default class AiSummaryModal extends Component {
|
||||
streamedTextLength = 0;
|
||||
|
||||
get outdatedSummaryWarningText() {
|
||||
let outdatedText = i18n("summary.outdated");
|
||||
let outdatedText = I18n.t("summary.outdated");
|
||||
|
||||
if (!this.topRepliesSummaryEnabled && this.newPostsSinceSummary > 0) {
|
||||
outdatedText += " ";
|
||||
outdatedText += i18n("summary.outdated_posts", {
|
||||
outdatedText += I18n.t("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 "discourse_ai.tools.test_modal.title"}}
|
||||
@title={{I18n.t "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 "discourse_ai.tools.test_modal.result"}}</h3>
|
||||
<h3>{{I18n.t "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