Compare commits
56
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c80396942 | ||
|
|
4dffd0b2c5 | ||
|
|
4c0660d6fd | ||
|
|
27de71fc4f | ||
|
|
478f31de47 | ||
|
|
4ce8973e56 | ||
|
|
34c98de864 | ||
|
|
38f7e9c2c4 | ||
|
|
e6876aabd5 | ||
|
|
b5d393b4bc | ||
|
|
77ae426d95 | ||
|
|
9f43df0302 | ||
|
|
59d6e2b467 | ||
|
|
c06d7b07d5 | ||
|
|
373e2305d6 | ||
|
|
ad5c48d9ae | ||
|
|
d99c335dab | ||
|
|
01e29ca5d8 | ||
|
|
297c64c3b8 | ||
|
|
ca78b1a1c5 | ||
|
|
39653aed22 | ||
|
|
6781ecd02e | ||
|
|
e264572597 | ||
|
|
70b0db2871 | ||
|
|
2d6ec5e1e6 | ||
|
|
cead887480 | ||
|
|
adc92e1e75 | ||
|
|
0ce17a122f | ||
|
|
cf220c530c | ||
|
|
61ef1932fa | ||
|
|
11e90f5f3f | ||
|
|
b817810d46 | ||
|
|
55dab9c68a | ||
|
|
c29183fc2d | ||
|
|
d72ad84f8f | ||
|
|
e207eba1a4 | ||
|
|
53905f65ac | ||
|
|
ef260be7cf | ||
|
|
0c69bb85ce | ||
|
|
af18603b21 | ||
|
|
7db2589cc4 | ||
|
|
2fb691cba8 | ||
|
|
de0625571b | ||
|
|
296aa24df1 | ||
|
|
3ac2359ff1 | ||
|
|
925949de47 | ||
|
|
b2fbab9fad | ||
|
|
381fa14158 | ||
|
|
77d3a38e49 | ||
|
|
7316058dfc | ||
|
|
9ee82fd8be | ||
|
|
dfea784fc4 | ||
|
|
ff2e18f9ca | ||
|
|
1b3fdad5c7 | ||
|
|
2c6459429f | ||
|
|
c34fcc8a95 |
@@ -50,7 +50,7 @@ module DiscourseAi
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
SiteSetting.ai_spam_detection_enabled = is_enabled
|
SiteSetting.set_and_log("ai_spam_detection_enabled", is_enabled, current_user)
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: AiSpamSerializer.new(spam_config, root: false)
|
render json: AiSpamSerializer.new(spam_config, root: false)
|
||||||
|
|||||||
@@ -42,6 +42,21 @@ module DiscourseAi
|
|||||||
<style>
|
<style>
|
||||||
#{artifact.css}
|
#{artifact.css}
|
||||||
</style>
|
</style>
|
||||||
|
<script>
|
||||||
|
window._discourse_user_data = {
|
||||||
|
#{current_user ? "username: #{current_user.username.to_json}" : "username: null"}
|
||||||
|
};
|
||||||
|
window.discourseArtifactReady = new Promise(resolve => {
|
||||||
|
window._resolveArtifactData = resolve;
|
||||||
|
});
|
||||||
|
window.addEventListener('message', function(event) {
|
||||||
|
if (event.data && event.data.type === 'discourse-artifact-data') {
|
||||||
|
window.discourseArtifactData = event.data.dataset || {};
|
||||||
|
Object.assign(window.discourseArtifactData, window._discourse_user_data);
|
||||||
|
window._resolveArtifactData(window.discourseArtifactData);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
#{artifact.html}
|
#{artifact.html}
|
||||||
@@ -74,6 +89,19 @@ module DiscourseAi
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<iframe sandbox="allow-scripts allow-forms" height="100%" width="100%" srcdoc="#{ERB::Util.html_escape(untrusted_html)}" frameborder="0"></iframe>
|
<iframe sandbox="allow-scripts allow-forms" height="100%" width="100%" srcdoc="#{ERB::Util.html_escape(untrusted_html)}" frameborder="0"></iframe>
|
||||||
|
<script>
|
||||||
|
document.querySelector('iframe').addEventListener('load', function() {
|
||||||
|
try {
|
||||||
|
const iframeWindow = this.contentWindow;
|
||||||
|
const message = { type: 'discourse-artifact-data', dataset: {} };
|
||||||
|
|
||||||
|
if (window.frameElement && window.frameElement.dataset) {
|
||||||
|
Object.assign(message.dataset, window.frameElement.dataset);
|
||||||
|
}
|
||||||
|
iframeWindow.postMessage(message, '*');
|
||||||
|
} catch (e) { console.error('Error passing data to artifact:', e); }
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
HTML
|
HTML
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ module DiscourseAi
|
|||||||
before_action :require_site_settings!
|
before_action :require_site_settings!
|
||||||
|
|
||||||
skip_before_action :preload_json, :check_xhr, only: %i[show asset]
|
skip_before_action :preload_json, :check_xhr, only: %i[show asset]
|
||||||
skip_before_action :verify_authenticity_token, only: ["asset"]
|
skip_before_action :redirect_to_login_if_required, :verify_authenticity_token, only: %i[asset]
|
||||||
|
|
||||||
def create
|
def create
|
||||||
ensure_allowed_create!
|
ensure_allowed_create!
|
||||||
|
|||||||
@@ -124,6 +124,10 @@ module DiscourseAi
|
|||||||
raise Discourse::InvalidParameters.new(:custom_prompt) if params[:custom_prompt].blank?
|
raise Discourse::InvalidParameters.new(:custom_prompt) if params[:custom_prompt].blank?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# to stream we must have an appropriate client_id
|
||||||
|
# otherwise we may end up streaming the data to the wrong client
|
||||||
|
raise Discourse::InvalidParameters.new(:client_id) if params[:client_id].blank?
|
||||||
|
|
||||||
if location == "composer"
|
if location == "composer"
|
||||||
Jobs.enqueue(
|
Jobs.enqueue(
|
||||||
:stream_composer_helper,
|
:stream_composer_helper,
|
||||||
@@ -132,6 +136,7 @@ module DiscourseAi
|
|||||||
prompt: prompt.name,
|
prompt: prompt.name,
|
||||||
custom_prompt: params[:custom_prompt],
|
custom_prompt: params[:custom_prompt],
|
||||||
force_default_locale: params[:force_default_locale] || false,
|
force_default_locale: params[:force_default_locale] || false,
|
||||||
|
client_id: params[:client_id],
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
post_id = get_post_param!
|
post_id = get_post_param!
|
||||||
@@ -146,6 +151,7 @@ module DiscourseAi
|
|||||||
text: text,
|
text: text,
|
||||||
prompt: prompt.name,
|
prompt: prompt.name,
|
||||||
custom_prompt: params[:custom_prompt],
|
custom_prompt: params[:custom_prompt],
|
||||||
|
client_id: params[:client_id],
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ module ::Jobs
|
|||||||
|
|
||||||
bot = DiscourseAi::Personas::Bot.as(bot_user, persona: persona.new)
|
bot = DiscourseAi::Personas::Bot.as(bot_user, persona: persona.new)
|
||||||
|
|
||||||
DiscourseAi::AiBot::Playground.new(bot).reply_to(post)
|
DiscourseAi::AiBot::Playground.new(bot).reply_to(post, feature_name: "bot")
|
||||||
rescue DiscourseAi::Personas::Bot::BOT_NOT_FOUND
|
rescue DiscourseAi::Personas::Bot::BOT_NOT_FOUND
|
||||||
Rails.logger.warn(
|
Rails.logger.warn(
|
||||||
"Bot not found for post #{post.id} - perhaps persona was deleted or bot was disabled",
|
"Bot not found for post #{post.id} - perhaps persona was deleted or bot was disabled",
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Jobs
|
||||||
|
class DetectTranslatePost < ::Jobs::Base
|
||||||
|
sidekiq_options retry: false
|
||||||
|
|
||||||
|
def execute(args)
|
||||||
|
return if !SiteSetting.discourse_ai_enabled
|
||||||
|
return if !SiteSetting.ai_translation_enabled
|
||||||
|
return if args[:post_id].blank?
|
||||||
|
|
||||||
|
post = Post.find_by(id: args[:post_id])
|
||||||
|
return if post.blank? || post.raw.blank? || post.deleted_at.present? || post.user_id <= 0
|
||||||
|
|
||||||
|
if SiteSetting.ai_translation_backfill_limit_to_public_content
|
||||||
|
topic = post.topic
|
||||||
|
if topic.blank? || topic.category&.read_restricted? ||
|
||||||
|
topic.archetype == Archetype.private_message
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
detected_locale = DiscourseAi::Translation::PostLocaleDetector.detect_locale(post)
|
||||||
|
rescue FinalDestination::SSRFDetector::LookupFailedError
|
||||||
|
# this job is non-critical
|
||||||
|
# the backfill job will handle failures
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
locales = SiteSetting.experimental_content_localization_supported_locales.split("|")
|
||||||
|
return if locales.blank?
|
||||||
|
|
||||||
|
locales.each do |locale|
|
||||||
|
next if locale == detected_locale
|
||||||
|
|
||||||
|
begin
|
||||||
|
DiscourseAi::Translation::PostLocalizer.localize(post, locale)
|
||||||
|
rescue FinalDestination::SSRFDetector::LookupFailedError
|
||||||
|
# do nothing, there are too many sporadic lookup failures
|
||||||
|
rescue => e
|
||||||
|
DiscourseAi::Translation::VerboseLogger.log(
|
||||||
|
"Failed to translate post #{post.id} to #{locale}: #{e.message}",
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
MessageBus.publish("/topic/#{post.topic_id}", type: :localized, id: post.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Jobs
|
||||||
|
class DetectTranslateTopic < ::Jobs::Base
|
||||||
|
sidekiq_options retry: false
|
||||||
|
|
||||||
|
def execute(args)
|
||||||
|
return if !SiteSetting.discourse_ai_enabled
|
||||||
|
return if !SiteSetting.ai_translation_enabled
|
||||||
|
return if args[:topic_id].blank?
|
||||||
|
|
||||||
|
topic = Topic.find_by(id: args[:topic_id])
|
||||||
|
if topic.blank? || topic.title.blank? || topic.deleted_at.present? || topic.user_id <= 0
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if SiteSetting.ai_translation_backfill_limit_to_public_content
|
||||||
|
return if topic.category&.read_restricted?
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
detected_locale = DiscourseAi::Translation::TopicLocaleDetector.detect_locale(topic)
|
||||||
|
rescue FinalDestination::SSRFDetector::LookupFailedError
|
||||||
|
# this job is non-critical
|
||||||
|
# the backfill job will handle failures
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
locales = SiteSetting.experimental_content_localization_supported_locales.split("|")
|
||||||
|
return if locales.blank?
|
||||||
|
|
||||||
|
locales.each do |locale|
|
||||||
|
next if locale == detected_locale
|
||||||
|
|
||||||
|
begin
|
||||||
|
DiscourseAi::Translation::TopicLocalizer.localize(topic, locale)
|
||||||
|
rescue FinalDestination::SSRFDetector::LookupFailedError
|
||||||
|
# do nothing, there are too many sporadic lookup failures
|
||||||
|
rescue => e
|
||||||
|
DiscourseAi::Translation::VerboseLogger.log(
|
||||||
|
"Failed to translate topic #{topic.id} to #{locale}: #{e.message}",
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
MessageBus.publish("/topic/#{topic.id}", type: :localized, id: 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Jobs
|
||||||
|
class GenerateInferredConcepts < ::Jobs::Base
|
||||||
|
sidekiq_options queue: "low"
|
||||||
|
|
||||||
|
# Process items to generate new concepts
|
||||||
|
#
|
||||||
|
# @param args [Hash] Contains job arguments
|
||||||
|
# @option args [String] :item_type Required - Type of items to process ('topics' or 'posts')
|
||||||
|
# @option args [Array<Integer>] :item_ids Required - List of item IDs to process
|
||||||
|
# @option args [Integer] :batch_size (100) Number of items to process in each batch
|
||||||
|
# @option args [Boolean] :match_only (false) Only match against existing concepts without generating new ones
|
||||||
|
def execute(args = {})
|
||||||
|
return if args[:item_ids].blank? || args[:item_type].blank?
|
||||||
|
|
||||||
|
if %w[topics posts].exclude?(args[:item_type])
|
||||||
|
Rails.logger.error("Invalid item_type for GenerateInferredConcepts: #{args[:item_type]}")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
# Process items in smaller batches to avoid memory issues
|
||||||
|
batch_size = args[:batch_size] || 100
|
||||||
|
|
||||||
|
# Get the list of item IDs
|
||||||
|
item_ids = args[:item_ids]
|
||||||
|
match_only = args[:match_only] || false
|
||||||
|
|
||||||
|
# Process items in batches
|
||||||
|
item_ids.each_slice(batch_size) do |batch_item_ids|
|
||||||
|
process_batch(batch_item_ids, args[:item_type], match_only)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def process_batch(item_ids, item_type, match_only)
|
||||||
|
klass = item_type.singularize.classify.constantize
|
||||||
|
items = klass.where(id: item_ids)
|
||||||
|
manager = DiscourseAi::InferredConcepts::Manager.new
|
||||||
|
|
||||||
|
items.each do |item|
|
||||||
|
begin
|
||||||
|
process_item(item, item_type, match_only, manager)
|
||||||
|
rescue => e
|
||||||
|
Rails.logger.error(
|
||||||
|
"Error generating concepts from #{item_type.singularize} #{item.id}: #{e.message}\n#{e.backtrace.join("\n")}",
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def process_item(item, item_type, match_only, manager)
|
||||||
|
# Use the Manager method that handles both identifying and creating concepts
|
||||||
|
if match_only
|
||||||
|
if item_type == "topics"
|
||||||
|
manager.match_topic_to_concepts(item)
|
||||||
|
else # posts
|
||||||
|
manager.match_post_to_concepts(item)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if item_type == "topics"
|
||||||
|
manager.generate_concepts_from_topic(item)
|
||||||
|
else # posts
|
||||||
|
manager.generate_concepts_from_post(item)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Jobs
|
||||||
|
class LocalizeCategories < ::Jobs::Base
|
||||||
|
cluster_concurrency 1
|
||||||
|
sidekiq_options retry: false
|
||||||
|
|
||||||
|
BATCH_SIZE = 50
|
||||||
|
|
||||||
|
def execute(args)
|
||||||
|
return if !SiteSetting.discourse_ai_enabled
|
||||||
|
return if !SiteSetting.ai_translation_enabled
|
||||||
|
|
||||||
|
locales = SiteSetting.experimental_content_localization_supported_locales.split("|")
|
||||||
|
return if locales.blank?
|
||||||
|
|
||||||
|
cat_id = args[:from_category_id] || Category.order(:id).first&.id
|
||||||
|
last_id = nil
|
||||||
|
|
||||||
|
categories = Category.where("id >= ?", cat_id).order(:id).limit(BATCH_SIZE)
|
||||||
|
return if categories.empty?
|
||||||
|
|
||||||
|
categories.each do |category|
|
||||||
|
if SiteSetting.ai_translation_backfill_limit_to_public_content && category.read_restricted?
|
||||||
|
last_id = category.id
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
CategoryLocalization.transaction do
|
||||||
|
locales.each do |locale|
|
||||||
|
next if CategoryLocalization.exists?(category_id: category.id, locale: locale)
|
||||||
|
begin
|
||||||
|
DiscourseAi::Translation::CategoryLocalizer.localize(category, locale)
|
||||||
|
rescue FinalDestination::SSRFDetector::LookupFailedError
|
||||||
|
# do nothing, there are too many sporadic lookup failures
|
||||||
|
rescue => e
|
||||||
|
DiscourseAi::Translation::VerboseLogger.log(
|
||||||
|
"Failed to translate category #{category.id} to #{locale}: #{e.message}",
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
last_id = category.id
|
||||||
|
end
|
||||||
|
|
||||||
|
if categories.size == BATCH_SIZE
|
||||||
|
Jobs.enqueue_in(10.seconds, :localize_categories, from_category_id: last_id + 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Jobs
|
||||||
|
class LocalizePosts < ::Jobs::Base
|
||||||
|
cluster_concurrency 1
|
||||||
|
sidekiq_options retry: false
|
||||||
|
|
||||||
|
BATCH_SIZE = 50
|
||||||
|
|
||||||
|
def execute(args)
|
||||||
|
return if !SiteSetting.discourse_ai_enabled
|
||||||
|
return if !SiteSetting.ai_translation_enabled
|
||||||
|
|
||||||
|
locales = SiteSetting.experimental_content_localization_supported_locales.split("|")
|
||||||
|
return if locales.blank?
|
||||||
|
|
||||||
|
limit = args[:limit] || BATCH_SIZE
|
||||||
|
|
||||||
|
locales.each do |locale|
|
||||||
|
posts =
|
||||||
|
Post
|
||||||
|
.joins(
|
||||||
|
"LEFT JOIN post_localizations pl ON pl.post_id = posts.id AND pl.locale = #{ActiveRecord::Base.connection.quote(locale)}",
|
||||||
|
)
|
||||||
|
.where(deleted_at: nil)
|
||||||
|
.where("posts.user_id > 0")
|
||||||
|
.where.not(raw: [nil, ""])
|
||||||
|
.where.not(locale: nil)
|
||||||
|
.where.not(locale: locale)
|
||||||
|
.where("pl.id IS NULL")
|
||||||
|
|
||||||
|
if SiteSetting.ai_translation_backfill_limit_to_public_content
|
||||||
|
posts =
|
||||||
|
posts
|
||||||
|
.joins(:topic)
|
||||||
|
.where(topics: { category_id: Category.where(read_restricted: false).select(:id) })
|
||||||
|
.where.not(topics: { archetype: Archetype.private_message })
|
||||||
|
end
|
||||||
|
|
||||||
|
if SiteSetting.ai_translation_backfill_max_age_days > 0
|
||||||
|
posts =
|
||||||
|
posts.where(
|
||||||
|
"posts.created_at > ?",
|
||||||
|
SiteSetting.ai_translation_backfill_max_age_days.days.ago,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
posts = posts.order(updated_at: :desc).limit(limit)
|
||||||
|
|
||||||
|
next if posts.empty?
|
||||||
|
|
||||||
|
posts.each do |post|
|
||||||
|
begin
|
||||||
|
DiscourseAi::Translation::PostLocalizer.localize(post, locale)
|
||||||
|
rescue FinalDestination::SSRFDetector::LookupFailedError
|
||||||
|
# do nothing, there are too many sporadic lookup failures
|
||||||
|
rescue => e
|
||||||
|
DiscourseAi::Translation::VerboseLogger.log(
|
||||||
|
"Failed to translate post #{post.id} to #{locale}: #{e.message}",
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
DiscourseAi::Translation::VerboseLogger.log("Translated #{posts.size} posts to #{locale}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Jobs
|
||||||
|
class LocalizeTopics < ::Jobs::Base
|
||||||
|
cluster_concurrency 1
|
||||||
|
sidekiq_options retry: false
|
||||||
|
|
||||||
|
BATCH_SIZE = 50
|
||||||
|
|
||||||
|
def execute(args)
|
||||||
|
return if !SiteSetting.discourse_ai_enabled
|
||||||
|
return if !SiteSetting.ai_translation_enabled
|
||||||
|
|
||||||
|
locales = SiteSetting.experimental_content_localization_supported_locales.split("|")
|
||||||
|
return if locales.blank?
|
||||||
|
|
||||||
|
limit = args[:limit] || BATCH_SIZE
|
||||||
|
|
||||||
|
locales.each do |locale|
|
||||||
|
topics =
|
||||||
|
Topic
|
||||||
|
.joins(
|
||||||
|
"LEFT JOIN topic_localizations tl ON tl.topic_id = topics.id AND tl.locale = #{ActiveRecord::Base.connection.quote(locale)}",
|
||||||
|
)
|
||||||
|
.where(deleted_at: nil)
|
||||||
|
.where("topics.user_id > 0")
|
||||||
|
.where.not(locale: nil)
|
||||||
|
.where.not(locale: locale)
|
||||||
|
.where("tl.id IS NULL")
|
||||||
|
|
||||||
|
if SiteSetting.ai_translation_backfill_limit_to_public_content
|
||||||
|
topics = topics.where(category_id: Category.where(read_restricted: false).select(:id))
|
||||||
|
end
|
||||||
|
|
||||||
|
if SiteSetting.ai_translation_backfill_max_age_days > 0
|
||||||
|
topics =
|
||||||
|
topics.where(
|
||||||
|
"topics.created_at > ?",
|
||||||
|
SiteSetting.ai_translation_backfill_max_age_days.days.ago,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
topics = topics.order(updated_at: :desc).limit(limit)
|
||||||
|
|
||||||
|
next if topics.empty?
|
||||||
|
|
||||||
|
topics.each do |topic|
|
||||||
|
begin
|
||||||
|
DiscourseAi::Translation::TopicLocalizer.localize(topic, locale)
|
||||||
|
rescue FinalDestination::SSRFDetector::LookupFailedError
|
||||||
|
# do nothing, there are too many sporadic lookup failures
|
||||||
|
rescue => e
|
||||||
|
DiscourseAi::Translation::VerboseLogger.log(
|
||||||
|
"Failed to translate topic #{topic.id} to #{locale}: #{e.message}",
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
DiscourseAi::Translation::VerboseLogger.log("Translated #{topics.size} topics to #{locale}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -8,6 +8,7 @@ module Jobs
|
|||||||
return unless args[:prompt]
|
return unless args[:prompt]
|
||||||
return unless user = User.find_by(id: args[:user_id])
|
return unless user = User.find_by(id: args[:user_id])
|
||||||
return unless args[:text]
|
return unless args[:text]
|
||||||
|
return unless args[:client_id]
|
||||||
|
|
||||||
prompt = CompletionPrompt.enabled_by_name(args[:prompt])
|
prompt = CompletionPrompt.enabled_by_name(args[:prompt])
|
||||||
|
|
||||||
@@ -21,6 +22,7 @@ module Jobs
|
|||||||
user,
|
user,
|
||||||
"/discourse-ai/ai-helper/stream_composer_suggestion",
|
"/discourse-ai/ai-helper/stream_composer_suggestion",
|
||||||
force_default_locale: args[:force_default_locale],
|
force_default_locale: args[:force_default_locale],
|
||||||
|
client_id: args[:client_id],
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Jobs
|
||||||
|
class CategoryLocalizationBackfill < ::Jobs::Scheduled
|
||||||
|
every 12.hours
|
||||||
|
cluster_concurrency 1
|
||||||
|
|
||||||
|
def execute(args)
|
||||||
|
return if !SiteSetting.discourse_ai_enabled
|
||||||
|
return if !SiteSetting.ai_translation_enabled
|
||||||
|
return if SiteSetting.experimental_content_localization_supported_locales.blank?
|
||||||
|
|
||||||
|
Jobs.enqueue(:localize_categories)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Jobs
|
||||||
|
class GenerateConceptsFromPopularItems < ::Jobs::Scheduled
|
||||||
|
every 1.day
|
||||||
|
|
||||||
|
# This job runs daily and generates new concepts from popular topics and posts
|
||||||
|
# It selects items based on engagement metrics and generates concepts from their content
|
||||||
|
def execute(_args)
|
||||||
|
return unless SiteSetting.inferred_concepts_enabled
|
||||||
|
|
||||||
|
process_popular_topics
|
||||||
|
process_popular_posts
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def process_popular_topics
|
||||||
|
# Find candidate topics that are popular and don't have concepts yet
|
||||||
|
manager = DiscourseAi::InferredConcepts::Manager.new
|
||||||
|
candidates =
|
||||||
|
manager.find_candidate_topics(
|
||||||
|
limit: SiteSetting.inferred_concepts_daily_topics_limit || 20,
|
||||||
|
min_posts: SiteSetting.inferred_concepts_min_posts || 5,
|
||||||
|
min_likes: SiteSetting.inferred_concepts_min_likes || 10,
|
||||||
|
min_views: SiteSetting.inferred_concepts_min_views || 100,
|
||||||
|
created_after: SiteSetting.inferred_concepts_lookback_days.days.ago,
|
||||||
|
)
|
||||||
|
|
||||||
|
return if candidates.blank?
|
||||||
|
|
||||||
|
# Process candidate topics - first generate concepts, then match
|
||||||
|
Jobs.enqueue(
|
||||||
|
:generate_inferred_concepts,
|
||||||
|
item_type: "topics",
|
||||||
|
item_ids: candidates.map(&:id),
|
||||||
|
batch_size: 10,
|
||||||
|
)
|
||||||
|
|
||||||
|
if SiteSetting.inferred_concepts_background_match
|
||||||
|
# Schedule a follow-up job to match existing concepts
|
||||||
|
Jobs.enqueue_in(
|
||||||
|
1.hour,
|
||||||
|
:generate_inferred_concepts,
|
||||||
|
item_type: "topics",
|
||||||
|
item_ids: candidates.map(&:id),
|
||||||
|
batch_size: 10,
|
||||||
|
match_only: true,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def process_popular_posts
|
||||||
|
# Find candidate posts that are popular and don't have concepts yet
|
||||||
|
manager = DiscourseAi::InferredConcepts::Manager.new
|
||||||
|
candidates =
|
||||||
|
manager.find_candidate_posts(
|
||||||
|
limit: SiteSetting.inferred_concepts_daily_posts_limit || 30,
|
||||||
|
min_likes: SiteSetting.inferred_concepts_post_min_likes || 5,
|
||||||
|
exclude_first_posts: true,
|
||||||
|
created_after: SiteSetting.inferred_concepts_lookback_days.days.ago,
|
||||||
|
)
|
||||||
|
|
||||||
|
return if candidates.blank?
|
||||||
|
|
||||||
|
# Process candidate posts - first generate concepts, then match
|
||||||
|
Jobs.enqueue(
|
||||||
|
:generate_inferred_concepts,
|
||||||
|
item_type: "posts",
|
||||||
|
item_ids: candidates.map(&:id),
|
||||||
|
batch_size: 10,
|
||||||
|
)
|
||||||
|
|
||||||
|
if SiteSetting.inferred_concepts_background_match
|
||||||
|
# Schedule a follow-up job to match against existing concepts
|
||||||
|
Jobs.enqueue_in(
|
||||||
|
1.hour,
|
||||||
|
:generate_inferred_concepts,
|
||||||
|
item_type: "posts",
|
||||||
|
item_ids: candidates.map(&:id),
|
||||||
|
batch_size: 10,
|
||||||
|
match_only: true,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Jobs
|
||||||
|
class PostLocalizationBackfill < ::Jobs::Scheduled
|
||||||
|
every 5.minutes
|
||||||
|
cluster_concurrency 1
|
||||||
|
|
||||||
|
def execute(args)
|
||||||
|
return if !SiteSetting.discourse_ai_enabled
|
||||||
|
return if !SiteSetting.ai_translation_enabled
|
||||||
|
|
||||||
|
return if SiteSetting.experimental_content_localization_supported_locales.blank?
|
||||||
|
return if SiteSetting.ai_translation_backfill_rate == 0
|
||||||
|
|
||||||
|
Jobs.enqueue(:localize_posts, limit: SiteSetting.ai_translation_backfill_rate)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Jobs
|
||||||
|
class PostsLocaleDetectionBackfill < ::Jobs::Scheduled
|
||||||
|
every 5.minutes
|
||||||
|
sidekiq_options retry: false
|
||||||
|
cluster_concurrency 1
|
||||||
|
|
||||||
|
def execute(args)
|
||||||
|
return if !SiteSetting.discourse_ai_enabled
|
||||||
|
return if !SiteSetting.ai_translation_enabled
|
||||||
|
return if SiteSetting.ai_translation_backfill_rate == 0
|
||||||
|
|
||||||
|
posts =
|
||||||
|
Post
|
||||||
|
.where(locale: nil)
|
||||||
|
.where(deleted_at: nil)
|
||||||
|
.where("posts.user_id > 0")
|
||||||
|
.where.not(raw: [nil, ""])
|
||||||
|
|
||||||
|
if SiteSetting.ai_translation_backfill_limit_to_public_content
|
||||||
|
public_categories = Category.where(read_restricted: false).pluck(:id)
|
||||||
|
posts =
|
||||||
|
posts
|
||||||
|
.joins(:topic)
|
||||||
|
.where(topics: { category_id: public_categories })
|
||||||
|
.where(topics: { archetype: "regular" })
|
||||||
|
end
|
||||||
|
|
||||||
|
if SiteSetting.ai_translation_backfill_max_age_days > 0
|
||||||
|
posts =
|
||||||
|
posts.where(
|
||||||
|
"posts.created_at > ?",
|
||||||
|
SiteSetting.ai_translation_backfill_max_age_days.days.ago,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
posts = posts.order(updated_at: :desc).limit(SiteSetting.ai_translation_backfill_rate)
|
||||||
|
return if posts.empty?
|
||||||
|
|
||||||
|
posts.each do |post|
|
||||||
|
begin
|
||||||
|
DiscourseAi::Translation::PostLocaleDetector.detect_locale(post)
|
||||||
|
rescue FinalDestination::SSRFDetector::LookupFailedError
|
||||||
|
# do nothing, there are too many sporadic lookup failures
|
||||||
|
rescue => e
|
||||||
|
DiscourseAi::Translation::VerboseLogger.log(
|
||||||
|
"Failed to detect post #{post.id}'s locale: #{e.message}",
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
DiscourseAi::Translation::VerboseLogger.log("Detected #{posts.size} post locales")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Jobs
|
||||||
|
class TopicLocalizationBackfill < ::Jobs::Scheduled
|
||||||
|
every 5.minutes
|
||||||
|
cluster_concurrency 1
|
||||||
|
|
||||||
|
def execute(args)
|
||||||
|
return if !SiteSetting.discourse_ai_enabled
|
||||||
|
return if !SiteSetting.ai_translation_enabled
|
||||||
|
|
||||||
|
return if SiteSetting.experimental_content_localization_supported_locales.blank?
|
||||||
|
return if SiteSetting.ai_translation_backfill_rate == 0
|
||||||
|
|
||||||
|
Jobs.enqueue(:localize_topics, limit: SiteSetting.ai_translation_backfill_rate)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Jobs
|
||||||
|
class TopicsLocaleDetectionBackfill < ::Jobs::Scheduled
|
||||||
|
every 5.minutes
|
||||||
|
sidekiq_options retry: false
|
||||||
|
cluster_concurrency 1
|
||||||
|
|
||||||
|
def execute(args)
|
||||||
|
return if !SiteSetting.discourse_ai_enabled
|
||||||
|
return if !SiteSetting.ai_translation_enabled
|
||||||
|
limit = SiteSetting.ai_translation_backfill_rate
|
||||||
|
return if limit == 0
|
||||||
|
|
||||||
|
topics = Topic.where(locale: nil, deleted_at: nil).where("topics.user_id > 0")
|
||||||
|
|
||||||
|
if SiteSetting.ai_translation_backfill_limit_to_public_content
|
||||||
|
topics = topics.where(category_id: Category.where(read_restricted: false).select(:id))
|
||||||
|
end
|
||||||
|
|
||||||
|
if SiteSetting.ai_translation_backfill_max_age_days > 0
|
||||||
|
topics =
|
||||||
|
topics.where(
|
||||||
|
"topics.created_at > ?",
|
||||||
|
SiteSetting.ai_translation_backfill_max_age_days.days.ago,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
topics = topics.order(updated_at: :desc).limit(limit)
|
||||||
|
return if topics.empty?
|
||||||
|
|
||||||
|
topics.each do |topic|
|
||||||
|
begin
|
||||||
|
DiscourseAi::Translation::TopicLocaleDetector.detect_locale(topic)
|
||||||
|
rescue FinalDestination::SSRFDetector::LookupFailedError
|
||||||
|
# do nothing, there are too many sporadic lookup failures
|
||||||
|
rescue => e
|
||||||
|
DiscourseAi::Translation::VerboseLogger.log(
|
||||||
|
"Failed to detect topic #{topic.id}'s locale: #{e.message}",
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
DiscourseAi::Translation::VerboseLogger.log("Detected #{topics.size} topic locales")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -266,6 +266,7 @@ class AiPersona < ActiveRecord::Base
|
|||||||
define_method(:top_p) { @ai_persona&.top_p }
|
define_method(:top_p) { @ai_persona&.top_p }
|
||||||
define_method(:system_prompt) { @ai_persona&.system_prompt || "You are a helpful bot." }
|
define_method(:system_prompt) { @ai_persona&.system_prompt || "You are a helpful bot." }
|
||||||
define_method(:uploads) { @ai_persona&.uploads }
|
define_method(:uploads) { @ai_persona&.uploads }
|
||||||
|
define_method(:response_format) { @ai_persona&.response_format }
|
||||||
define_method(:examples) { @ai_persona&.examples }
|
define_method(:examples) { @ai_persona&.examples }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ class AiTool < ActiveRecord::Base
|
|||||||
message: I18n.t("discourse_ai.tools.name.characters"),
|
message: I18n.t("discourse_ai.tools.name.characters"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validate :validate_parameters_enum
|
||||||
|
|
||||||
def signature
|
def signature
|
||||||
{
|
{
|
||||||
name: function_call_name,
|
name: function_call_name,
|
||||||
@@ -57,6 +59,30 @@ class AiTool < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def validate_parameters_enum
|
||||||
|
return unless parameters.is_a?(Array)
|
||||||
|
|
||||||
|
parameters.each_with_index do |param, index|
|
||||||
|
next if !param.is_a?(Hash) || !param.key?("enum")
|
||||||
|
enum_values = param["enum"]
|
||||||
|
|
||||||
|
if enum_values.empty?
|
||||||
|
errors.add(
|
||||||
|
:parameters,
|
||||||
|
"Parameter '#{param["name"]}' at index #{index}: enum cannot be empty",
|
||||||
|
)
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
if enum_values.uniq.length != enum_values.length
|
||||||
|
errors.add(
|
||||||
|
:parameters,
|
||||||
|
"Parameter '#{param["name"]}' at index #{index}: enum values must be unique",
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def self.preamble
|
def self.preamble
|
||||||
<<~JS
|
<<~JS
|
||||||
/**
|
/**
|
||||||
@@ -142,6 +168,7 @@ class AiTool < ActiveRecord::Base
|
|||||||
* base_64_content (string): Base64 encoded content of the file.
|
* base_64_content (string): Base64 encoded content of the file.
|
||||||
* Returns: { id: number, url: string, short_url: string } - Details of the created upload record.
|
* Returns: { id: number, url: string, short_url: string } - Details of the created upload record.
|
||||||
*
|
*
|
||||||
|
* upload.getUrl(shortUrl): Given a short URL, eg upload://12345, returns the full CDN friendly URL of the upload.
|
||||||
* 5. chain
|
* 5. chain
|
||||||
* Controls the execution flow.
|
* Controls the execution flow.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class InferredConcept < ActiveRecord::Base
|
||||||
|
has_many :inferred_concept_topics
|
||||||
|
has_many :topics, through: :inferred_concept_topics
|
||||||
|
|
||||||
|
has_many :inferred_concept_posts
|
||||||
|
has_many :posts, through: :inferred_concept_posts
|
||||||
|
|
||||||
|
validates :name, presence: true, uniqueness: true
|
||||||
|
end
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: inferred_concepts
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# name :string not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_inferred_concepts_on_name (name) UNIQUE
|
||||||
|
#
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class InferredConceptPost < ActiveRecord::Base
|
||||||
|
belongs_to :inferred_concept
|
||||||
|
belongs_to :post
|
||||||
|
|
||||||
|
validates :inferred_concept_id, presence: true
|
||||||
|
validates :post_id, presence: true
|
||||||
|
validates :inferred_concept_id, uniqueness: { scope: :post_id }
|
||||||
|
end
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: inferred_concept_posts
|
||||||
|
#
|
||||||
|
# inferred_concept_id :bigint
|
||||||
|
# post_id :bigint
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_inferred_concept_posts_on_inferred_concept_id (inferred_concept_id)
|
||||||
|
# index_inferred_concept_posts_uniqueness (post_id,inferred_concept_id) UNIQUE
|
||||||
|
#
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class InferredConceptTopic < ActiveRecord::Base
|
||||||
|
belongs_to :inferred_concept
|
||||||
|
belongs_to :topic
|
||||||
|
|
||||||
|
validates :inferred_concept_id, presence: true
|
||||||
|
validates :topic_id, presence: true
|
||||||
|
validates :inferred_concept_id, uniqueness: { scope: :topic_id }
|
||||||
|
end
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: inferred_concept_topics
|
||||||
|
#
|
||||||
|
# inferred_concept_id :bigint
|
||||||
|
# topic_id :bigint
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_inferred_concept_topics_on_inferred_concept_id (inferred_concept_id)
|
||||||
|
# index_inferred_concept_topics_uniqueness (topic_id,inferred_concept_id) UNIQUE
|
||||||
|
#
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AiInferredConceptPostSerializer < ApplicationSerializer
|
||||||
|
attributes :id,
|
||||||
|
:post_number,
|
||||||
|
:topic_id,
|
||||||
|
:topic_title,
|
||||||
|
:username,
|
||||||
|
:avatar_template,
|
||||||
|
:created_at,
|
||||||
|
:updated_at,
|
||||||
|
:excerpt,
|
||||||
|
:truncated,
|
||||||
|
:inferred_concepts
|
||||||
|
|
||||||
|
def avatar_template
|
||||||
|
User.avatar_template(object.username, object.uploaded_avatar_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def excerpt
|
||||||
|
Post.excerpt(object.cooked)
|
||||||
|
end
|
||||||
|
|
||||||
|
def truncated
|
||||||
|
object.cooked.length > SiteSetting.post_excerpt_maxlength
|
||||||
|
end
|
||||||
|
|
||||||
|
def inferred_concepts
|
||||||
|
ActiveModel::ArraySerializer.new(
|
||||||
|
object.inferred_concepts,
|
||||||
|
each_serializer: InferredConceptSerializer,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class InferredConceptSerializer < ApplicationSerializer
|
||||||
|
attributes :id, :name, :created_at, :updated_at
|
||||||
|
end
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import Component from "@glimmer/component";
|
import Component from "@glimmer/component";
|
||||||
import { tracked } from "@glimmer/tracking";
|
import { tracked } from "@glimmer/tracking";
|
||||||
import { action } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
|
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
||||||
import { service } from "@ember/service";
|
import { service } from "@ember/service";
|
||||||
import DButton from "discourse/components/d-button";
|
import DButton from "discourse/components/d-button";
|
||||||
import htmlClass from "discourse/helpers/html-class";
|
import htmlClass from "discourse/helpers/html-class";
|
||||||
@@ -88,6 +89,15 @@ export default class AiArtifactComponent extends Component {
|
|||||||
}`;
|
}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
setDataAttributes(element) {
|
||||||
|
if (this.args.dataAttributes) {
|
||||||
|
Object.entries(this.args.dataAttributes).forEach(([key, value]) => {
|
||||||
|
element.setAttribute(key, value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
{{#if this.expanded}}
|
{{#if this.expanded}}
|
||||||
{{htmlClass "ai-artifact-expanded"}}
|
{{htmlClass "ai-artifact-expanded"}}
|
||||||
@@ -118,6 +128,7 @@ export default class AiArtifactComponent extends Component {
|
|||||||
src={{this.artifactUrl}}
|
src={{this.artifactUrl}}
|
||||||
width="100%"
|
width="100%"
|
||||||
frameborder="0"
|
frameborder="0"
|
||||||
|
{{didInsert this.setDataAttributes}}
|
||||||
></iframe>
|
></iframe>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#unless this.requireClickToRun}}
|
{{#unless this.requireClickToRun}}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,7 @@ import { composeAiBotMessage } from "../lib/ai-bot-helper";
|
|||||||
import { AI_CONVERSATIONS_PANEL } from "../services/ai-conversations-sidebar-manager";
|
import { AI_CONVERSATIONS_PANEL } from "../services/ai-conversations-sidebar-manager";
|
||||||
|
|
||||||
export default class AiBotHeaderIcon extends Component {
|
export default class AiBotHeaderIcon extends Component {
|
||||||
|
@service appEvents;
|
||||||
@service composer;
|
@service composer;
|
||||||
@service currentUser;
|
@service currentUser;
|
||||||
@service navigationMenu;
|
@service navigationMenu;
|
||||||
@@ -51,6 +52,7 @@ export default class AiBotHeaderIcon extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.siteSettings.ai_bot_enable_dedicated_ux) {
|
if (this.siteSettings.ai_bot_enable_dedicated_ux) {
|
||||||
|
this.appEvents.trigger("discourse-ai:bot-header-icon-clicked");
|
||||||
return this.router.transitionTo("discourse-ai-bot-conversations");
|
return this.router.transitionTo("discourse-ai-bot-conversations");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import DButton from "discourse/components/d-button";
|
|||||||
import { AI_CONVERSATIONS_PANEL } from "../services/ai-conversations-sidebar-manager";
|
import { AI_CONVERSATIONS_PANEL } from "../services/ai-conversations-sidebar-manager";
|
||||||
|
|
||||||
export default class AiBotSidebarNewConversation extends Component {
|
export default class AiBotSidebarNewConversation extends Component {
|
||||||
|
@service appEvents;
|
||||||
@service router;
|
@service router;
|
||||||
@service sidebarState;
|
@service sidebarState;
|
||||||
|
|
||||||
@@ -14,6 +15,8 @@ export default class AiBotSidebarNewConversation extends Component {
|
|||||||
|
|
||||||
@action
|
@action
|
||||||
routeTo() {
|
routeTo() {
|
||||||
|
this.appEvents.trigger("discourse-ai:new-conversation-btn-clicked");
|
||||||
|
|
||||||
if (this.router.currentRouteName !== "discourse-ai-bot-conversations") {
|
if (this.router.currentRouteName !== "discourse-ai-bot-conversations") {
|
||||||
this.router.transitionTo("/discourse-ai/ai-bot/conversations");
|
this.router.transitionTo("/discourse-ai/ai-bot/conversations");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export default class AiPersonaCollapsableExample extends Component {
|
|||||||
(if (eq pairIdx 0) "user" "model")
|
(if (eq pairIdx 0) "user" "model")
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
@validation="required|length:1,100"
|
@validation="required|length:1,5000"
|
||||||
@disabled={{@system}}
|
@disabled={{@system}}
|
||||||
as |field|
|
as |field|
|
||||||
>
|
>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user