Compare commits
33
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d8895dad1 | ||
|
|
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 |
@@ -50,7 +50,7 @@ module DiscourseAi
|
||||
)
|
||||
end
|
||||
|
||||
SiteSetting.ai_spam_detection_enabled = is_enabled
|
||||
SiteSetting.set_and_log("ai_spam_detection_enabled", is_enabled, current_user)
|
||||
end
|
||||
|
||||
render json: AiSpamSerializer.new(spam_config, root: false)
|
||||
|
||||
@@ -124,6 +124,10 @@ module DiscourseAi
|
||||
raise Discourse::InvalidParameters.new(:custom_prompt) if params[:custom_prompt].blank?
|
||||
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"
|
||||
Jobs.enqueue(
|
||||
:stream_composer_helper,
|
||||
@@ -132,6 +136,7 @@ module DiscourseAi
|
||||
prompt: prompt.name,
|
||||
custom_prompt: params[:custom_prompt],
|
||||
force_default_locale: params[:force_default_locale] || false,
|
||||
client_id: params[:client_id],
|
||||
)
|
||||
else
|
||||
post_id = get_post_param!
|
||||
@@ -146,6 +151,7 @@ module DiscourseAi
|
||||
text: text,
|
||||
prompt: prompt.name,
|
||||
custom_prompt: params[:custom_prompt],
|
||||
client_id: params[:client_id],
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
@@ -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,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 user = User.find_by(id: args[:user_id])
|
||||
return unless args[:text]
|
||||
return unless args[:client_id]
|
||||
|
||||
prompt = CompletionPrompt.enabled_by_name(args[:prompt])
|
||||
|
||||
@@ -21,6 +22,7 @@ module Jobs
|
||||
user,
|
||||
"/discourse-ai/ai-helper/stream_composer_suggestion",
|
||||
force_default_locale: args[:force_default_locale],
|
||||
client_id: args[:client_id],
|
||||
)
|
||||
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,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
|
||||
@@ -5,6 +5,7 @@ import DButton from "discourse/components/d-button";
|
||||
import { AI_CONVERSATIONS_PANEL } from "../services/ai-conversations-sidebar-manager";
|
||||
|
||||
export default class AiBotSidebarNewConversation extends Component {
|
||||
@service appEvents;
|
||||
@service router;
|
||||
@service sidebarState;
|
||||
|
||||
@@ -14,6 +15,8 @@ export default class AiBotSidebarNewConversation extends Component {
|
||||
|
||||
@action
|
||||
routeTo() {
|
||||
this.appEvents.trigger("discourse-ai:new-conversation-btn-clicked");
|
||||
|
||||
if (this.router.currentRouteName !== "discourse-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")
|
||||
)
|
||||
}}
|
||||
@validation="required|length:1,100"
|
||||
@validation="required|length:1,5000"
|
||||
@disabled={{@system}}
|
||||
as |field|
|
||||
>
|
||||
|
||||
@@ -242,6 +242,7 @@ export default class AiPostHelperMenu extends Component {
|
||||
text: this.args.data.selectedText,
|
||||
post_id: this.args.data.quoteState.postId,
|
||||
custom_prompt: this.customPromptValue,
|
||||
client_id: this.messageBus.clientId,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -12,18 +12,21 @@ import concatClass from "discourse/helpers/concat-class";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { bind } from "discourse/lib/decorators";
|
||||
import { escapeExpression } from "discourse/lib/utilities";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import DiffStreamer from "../../lib/diff-streamer";
|
||||
import SmoothStreamer from "../../lib/smooth-streamer";
|
||||
import AiIndicatorWave from "../ai-indicator-wave";
|
||||
|
||||
const CHANNEL = "/discourse-ai/ai-helper/stream_composer_suggestion";
|
||||
|
||||
export default class ModalDiffModal extends Component {
|
||||
@service currentUser;
|
||||
@service messageBus;
|
||||
|
||||
@tracked loading = false;
|
||||
@tracked finalResult = "";
|
||||
@tracked showcasedDiff = "";
|
||||
@tracked escapedSelectedText = escapeExpression(this.args.model.selectedText);
|
||||
@tracked diffStreamer = new DiffStreamer(this.args.model.selectedText);
|
||||
@tracked suggestion = "";
|
||||
@tracked
|
||||
@@ -31,13 +34,28 @@ export default class ModalDiffModal extends Component {
|
||||
() => this.suggestion,
|
||||
(newValue) => (this.suggestion = newValue)
|
||||
);
|
||||
@tracked isStreaming = false;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.suggestChanges();
|
||||
}
|
||||
|
||||
get diffResult() {
|
||||
if (this.diffStreamer.diff?.length > 0) {
|
||||
return this.diffStreamer.diff;
|
||||
}
|
||||
|
||||
// Prevents flash by showing the
|
||||
// original text when the diff is empty
|
||||
return this.escapedSelectedText;
|
||||
}
|
||||
|
||||
get isStreaming() {
|
||||
// diffStreamer stops Streaming when it is finished with a chunk, looking at isDone is safe
|
||||
// it starts off not done
|
||||
return !this.diffStreamer.isDone || this.smoothStreamer.isStreaming;
|
||||
}
|
||||
|
||||
get primaryBtnLabel() {
|
||||
return this.loading
|
||||
? i18n("discourse_ai.ai_helper.context_menu.loading")
|
||||
@@ -50,35 +68,24 @@ export default class ModalDiffModal extends Component {
|
||||
|
||||
@bind
|
||||
subscribe() {
|
||||
const channel = "/discourse-ai/ai-helper/stream_composer_suggestion";
|
||||
this.messageBus.subscribe(channel, this.updateResult);
|
||||
this.messageBus.subscribe(CHANNEL, this.updateResult);
|
||||
}
|
||||
|
||||
@bind
|
||||
unsubscribe() {
|
||||
const channel = "/discourse-ai/ai-helper/stream_composer_suggestion";
|
||||
this.messageBus.subscribe(channel, this.updateResult);
|
||||
cleanup() {
|
||||
// stop all callbacks so it does not end up streaming pointlessly
|
||||
this.smoothStreamer.resetStreaming();
|
||||
this.diffStreamer.reset();
|
||||
this.messageBus.unsubscribe(CHANNEL, this.updateResult);
|
||||
}
|
||||
|
||||
@action
|
||||
async updateResult(result) {
|
||||
// TODO(@keegan)
|
||||
// Temporarily we are removing the animation using the diff streamer
|
||||
// and simply showing the diff streamed without a proper animation
|
||||
// while we figure things out
|
||||
// so that things are not too janky in the meantime.
|
||||
updateResult(result) {
|
||||
this.loading = false;
|
||||
this.isStreaming = true;
|
||||
|
||||
if (result.done) {
|
||||
this.finalResult = result.result;
|
||||
}
|
||||
|
||||
this.showcasedDiff = result.diff;
|
||||
|
||||
if (result.done) {
|
||||
this.loading = false;
|
||||
this.isStreaming = false;
|
||||
}
|
||||
|
||||
if (this.args.model.showResultAsDiff) {
|
||||
@@ -103,6 +110,7 @@ export default class ModalDiffModal extends Component {
|
||||
text: this.args.model.selectedText,
|
||||
custom_prompt: this.args.model.customPromptValue,
|
||||
force_default_locale: true,
|
||||
client_id: this.messageBus.clientId,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
@@ -125,6 +133,7 @@ export default class ModalDiffModal extends Component {
|
||||
this.finalResult?.length > 0
|
||||
? this.finalResult
|
||||
: this.diffStreamer.suggestion;
|
||||
|
||||
if (this.args.model.showResultAsDiff && finalResult) {
|
||||
this.args.model.toolbarEvent.replaceText(
|
||||
this.args.model.selectedText,
|
||||
@@ -140,10 +149,14 @@ export default class ModalDiffModal extends Component {
|
||||
@closeModal={{@closeModal}}
|
||||
>
|
||||
<:body>
|
||||
<div {{didInsert this.subscribe}} {{willDestroy this.unsubscribe}}>
|
||||
<div
|
||||
{{didInsert this.subscribe}}
|
||||
{{willDestroy this.cleanup}}
|
||||
class="text-preview"
|
||||
>
|
||||
{{#if this.loading}}
|
||||
<div class="composer-ai-helper-modal__loading">
|
||||
<CookText @rawText={{@model.selectedText}} />
|
||||
{{~@model.selectedText~}}
|
||||
</div>
|
||||
{{else}}
|
||||
<div
|
||||
@@ -152,10 +165,11 @@ export default class ModalDiffModal extends Component {
|
||||
"streamable-content"
|
||||
(if this.isStreaming "streaming")
|
||||
(if @model.showResultAsDiff "inline-diff")
|
||||
(if this.diffStreamer.isThinking "thinking")
|
||||
}}
|
||||
>
|
||||
{{#if @model.showResultAsDiff}}
|
||||
{{htmlSafe this.showcasedDiff}}
|
||||
{{~#if @model.showResultAsDiff~}}
|
||||
<span class="diff-inner">{{htmlSafe this.diffResult}}</span>
|
||||
{{else}}
|
||||
{{#if this.smoothStreamer.isStreaming}}
|
||||
<CookText
|
||||
|
||||
+9
-3
@@ -19,7 +19,6 @@ export default class AiPostHelperTrigger extends Component {
|
||||
@tracked menuState = this.MENU_STATES.triggers;
|
||||
@tracked showMainButtons = true;
|
||||
@tracked showAiButtons = true;
|
||||
@tracked originalPostHTML = null;
|
||||
@tracked postHighlighted = false;
|
||||
@tracked currentMenu = this.menu.getByIdentifier(
|
||||
"post-text-selection-toolbar"
|
||||
@@ -45,7 +44,6 @@ export default class AiPostHelperTrigger extends Component {
|
||||
return;
|
||||
}
|
||||
|
||||
this.originalPostHTML = postElement.innerHTML;
|
||||
this.selectedText = this.args.outletArgs.data.quoteState.buffer;
|
||||
|
||||
const selection = window.getSelection();
|
||||
@@ -121,7 +119,15 @@ export default class AiPostHelperTrigger extends Component {
|
||||
return;
|
||||
}
|
||||
|
||||
postElement.innerHTML = this.originalPostHTML;
|
||||
const highlightedSpans = postElement.querySelectorAll(
|
||||
"span.ai-helper-highlighted-selection"
|
||||
);
|
||||
|
||||
highlightedSpans.forEach((span) => {
|
||||
const textNode = document.createTextNode(span.textContent);
|
||||
span.parentNode.replaceChild(textNode, span);
|
||||
});
|
||||
|
||||
this.postHighlighted = false;
|
||||
}
|
||||
|
||||
|
||||
+16
-10
@@ -7,6 +7,10 @@ import AiSummaryModal from "../../components/modal/ai-summary-modal";
|
||||
export default class AiSummaryTrigger extends Component {
|
||||
@service modal;
|
||||
|
||||
get isAiConversation() {
|
||||
return this.args.outletArgs.topic.is_bot_pm;
|
||||
}
|
||||
|
||||
@action
|
||||
openAiSummaryModal() {
|
||||
this.modal.show(AiSummaryModal, {
|
||||
@@ -18,15 +22,17 @@ export default class AiSummaryTrigger extends Component {
|
||||
}
|
||||
|
||||
<template>
|
||||
{{#if @outletArgs.topic.summarizable}}
|
||||
<section class="topic-map__additional-contents toggle-summary">
|
||||
<DButton
|
||||
@label="summary.buttons.generate"
|
||||
@icon="discourse-sparkles"
|
||||
@action={{this.openAiSummaryModal}}
|
||||
class="btn-default ai-summarization-button"
|
||||
/>
|
||||
</section>
|
||||
{{/if}}
|
||||
{{#unless this.isAiConversation}}
|
||||
{{#if @outletArgs.topic.summarizable}}
|
||||
<section class="topic-map__additional-contents toggle-summary">
|
||||
<DButton
|
||||
@label="summary.buttons.generate"
|
||||
@icon="discourse-sparkles"
|
||||
@action={{this.openAiSummaryModal}}
|
||||
class="btn-default ai-summarization-button"
|
||||
/>
|
||||
</section>
|
||||
{{/if}}
|
||||
{{/unless}}
|
||||
</template>
|
||||
}
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { action } from "@ember/object";
|
||||
import { service } from "@ember/service";
|
||||
import { or } from "truth-helpers";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import Participant from "discourse/components/header/topic/participant";
|
||||
import AddPmParticipants from "discourse/components/modal/add-pm-participants";
|
||||
|
||||
export default class AiConversationInvite extends Component {
|
||||
static shouldRender(args) {
|
||||
return args.topic.is_bot_pm;
|
||||
}
|
||||
|
||||
@service site;
|
||||
@service modal;
|
||||
@service header;
|
||||
@service sidebarState;
|
||||
|
||||
get participants() {
|
||||
const participants = [
|
||||
...(this.header.topicInfo.details?.allowed_users ?? []),
|
||||
...(this.header.topicInfo.details?.allowed_groups ?? []),
|
||||
];
|
||||
return participants;
|
||||
}
|
||||
|
||||
@action
|
||||
showInvite() {
|
||||
this.modal.show(AddPmParticipants, {
|
||||
model: {
|
||||
title: "discourse_ai.ai_bot.invite_ai_conversation.title",
|
||||
inviteModel: this.args.outletArgs.topic,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="ai-conversation__participants">
|
||||
<DButton
|
||||
@icon="user-plus"
|
||||
@label="discourse_ai.ai_bot.invite_ai_conversation.button"
|
||||
@action={{this.showInvite}}
|
||||
class="btn-default ai-conversations__invite-button"
|
||||
/>
|
||||
{{#each this.participants as |participant|}}
|
||||
<Participant
|
||||
@user={{participant}}
|
||||
@type={{if participant.username "user" "group"}}
|
||||
@username={{or participant.username participant.name}}
|
||||
@avatarSize="medium"
|
||||
/>
|
||||
{{/each}}
|
||||
</div>
|
||||
</template>
|
||||
}
|
||||
@@ -12,13 +12,15 @@ export default class PostUpdater extends StreamUpdater {
|
||||
|
||||
constructor(postStream, postId) {
|
||||
super();
|
||||
|
||||
this.postStream = postStream;
|
||||
this.postId = postId;
|
||||
this.post = postStream.findLoadedPost(postId);
|
||||
const topicId = postStream.topic.id;
|
||||
|
||||
if (this.post) {
|
||||
this.postElement = document.querySelector(
|
||||
`#post_${this.post.post_number}`
|
||||
`.topic-area[data-topic-id="${topicId}"] #post_${this.post.post_number}`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -57,6 +59,10 @@ export default class PostUpdater extends StreamUpdater {
|
||||
}
|
||||
|
||||
async setCooked(value) {
|
||||
if (!this.postElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.post.set("cooked", value);
|
||||
|
||||
(await loadMorphlex()).morphInner(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -42,8 +42,7 @@ export default class AiConversationsSidebarManager extends Service {
|
||||
if (this.sidebarState.isForcingSidebar && !isAdminSidebarActive) {
|
||||
this.sidebarState.setPanel(MAIN_PANEL); // Return to main sidebar panel
|
||||
this.sidebarState.isForcingSidebar = false;
|
||||
this.appEvents.trigger("discourse-ai:stop-forcing-conversations-sidebar");
|
||||
}
|
||||
|
||||
this.appEvents.trigger("discourse-ai:stop-forcing-conversations-sidebar");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ function initializeAiHelperTrigger(api) {
|
||||
icon: "discourse-sparkles",
|
||||
title: "discourse_ai.ai_helper.context_menu.trigger",
|
||||
preventFocus: true,
|
||||
hideShortcutInTitle: true,
|
||||
shortcut: "ALT+P",
|
||||
shortcutAction: (toolbarEvent) => {
|
||||
if (toolbarEvent.getText().length === 0) {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { apiInitializer } from "discourse/lib/api";
|
||||
import cookie from "discourse/lib/cookie";
|
||||
|
||||
export default apiInitializer((api) => {
|
||||
const settings = api.container.lookup("service:site-settings");
|
||||
|
||||
if (!settings.discourse_ai_enabled || !settings.ai_translation_enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
api.registerCustomPostMessageCallback(
|
||||
"localized",
|
||||
(topicController, data) => {
|
||||
if (!cookie("content-localization-show-original")) {
|
||||
const postStream = topicController.get("model.postStream");
|
||||
postStream.triggerChangedPost(data.id, data.updated_at).then(() => {
|
||||
topicController.appEvents.trigger("post-stream:refresh", {
|
||||
id: data.id,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -83,14 +83,48 @@ article.streaming .cooked {
|
||||
@keyframes mark-blink {
|
||||
0%,
|
||||
100% {
|
||||
border-color: var(--highlight-high);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
50% {
|
||||
border-color: transparent;
|
||||
border-color: var(--highlight-high);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-in-highlight {
|
||||
from {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
mark.highlight {
|
||||
background-color: var(--highlight-high);
|
||||
animation: fade-in-highlight 0.5s ease-in-out forwards;
|
||||
}
|
||||
|
||||
.composer-ai-helper-modal__suggestion.thinking mark.highlight {
|
||||
animation: mark-blink 1s step-start 0s infinite;
|
||||
animation-name: mark-blink;
|
||||
}
|
||||
|
||||
.composer-ai-helper-modal__loading {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.composer-ai-helper-modal__suggestion.inline-diff {
|
||||
white-space: pre-wrap;
|
||||
|
||||
del:last-child {
|
||||
text-decoration: none;
|
||||
background-color: transparent;
|
||||
color: var(--primary-low-mid);
|
||||
}
|
||||
|
||||
.diff-inner {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +78,6 @@ body.has-ai-conversations-sidebar {
|
||||
.private-message-glyph-wrapper,
|
||||
.topic-header-participants,
|
||||
.topic-above-footer-buttons-outlet,
|
||||
.topic-map,
|
||||
#topic-footer-buttons .topic-footer-main-buttons details {
|
||||
display: none;
|
||||
}
|
||||
@@ -455,4 +454,82 @@ body.has-ai-conversations-sidebar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.topic-map {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
padding-block: 0.5em;
|
||||
|
||||
.topic-map__views-trigger,
|
||||
.topic-map__likes-trigger,
|
||||
.summarization-button,
|
||||
&__private-message-map,
|
||||
.topic-map__users-list {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.--bottom {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
section {
|
||||
background: transparent;
|
||||
border-block: 1px solid var(--primary-low);
|
||||
}
|
||||
|
||||
&__contents {
|
||||
padding: 0.5em 1.25em;
|
||||
|
||||
@include viewport.from(sm) {
|
||||
padding: 0.5em 0.5em 0.5em 1.9em;
|
||||
}
|
||||
}
|
||||
|
||||
&__stats {
|
||||
height: 100%;
|
||||
flex-wrap: nowrap;
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
.ai-conversation__participants {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5em 0.25em;
|
||||
align-items: center;
|
||||
|
||||
.avatar {
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
}
|
||||
|
||||
.trigger-group-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid var(--primary-low);
|
||||
border-radius: var(--d-button-border-radius);
|
||||
padding: 0.25em 0.5em;
|
||||
font-size: var(--font-down-1);
|
||||
|
||||
span {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
.d-icon {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--primary-medium);
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
font-size: var(--font-down-1);
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
@use "lib/viewport";
|
||||
|
||||
.composer-ai-helper-modal {
|
||||
.text-preview,
|
||||
.inline-diff {
|
||||
font-family: var(--d-font-family--monospace);
|
||||
font-variant-ligatures: none;
|
||||
|
||||
ins {
|
||||
background-color: var(--success-low);
|
||||
text-decoration: none;
|
||||
@@ -176,7 +181,7 @@
|
||||
|
||||
@keyframes dot-falling {
|
||||
0% {
|
||||
box-shadow: 9999px -15px 0 0 rgba(152, 128, 255, 0);
|
||||
box-shadow: 9999px -15px 0 0 rgb(152, 128, 255, 0);
|
||||
}
|
||||
|
||||
25%,
|
||||
@@ -186,13 +191,13 @@
|
||||
}
|
||||
|
||||
100% {
|
||||
box-shadow: 9999px 15px 0 0 rgba(152, 128, 255, 0);
|
||||
box-shadow: 9999px 15px 0 0 rgb(152, 128, 255, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dot-falling-before {
|
||||
0% {
|
||||
box-shadow: 9984px -15px 0 0 rgba(152, 128, 255, 0);
|
||||
box-shadow: 9984px -15px 0 0 rgb(152, 128, 255, 0);
|
||||
}
|
||||
|
||||
25%,
|
||||
@@ -202,13 +207,13 @@
|
||||
}
|
||||
|
||||
100% {
|
||||
box-shadow: 9984px 15px 0 0 rgba(152, 128, 255, 0);
|
||||
box-shadow: 9984px 15px 0 0 rgb(152, 128, 255, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dot-falling-after {
|
||||
0% {
|
||||
box-shadow: 10014px -15px 0 0 rgba(152, 128, 255, 0);
|
||||
box-shadow: 10014px -15px 0 0 rgb(152, 128, 255, 0);
|
||||
}
|
||||
|
||||
25%,
|
||||
@@ -218,7 +223,7 @@
|
||||
}
|
||||
|
||||
100% {
|
||||
box-shadow: 10014px 15px 0 0 rgba(152, 128, 255, 0);
|
||||
box-shadow: 10014px 15px 0 0 rgb(152, 128, 255, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,19 +234,6 @@
|
||||
input:focus {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
input {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.category-chooser,
|
||||
.mini-tag-chooser {
|
||||
.select-kit-header {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
#edit-title {
|
||||
@@ -636,7 +628,7 @@
|
||||
height: 7em;
|
||||
min-width: var(--ai-caption-popup-min-width);
|
||||
|
||||
@include breakpoint(tablet) {
|
||||
@include viewport.until(md) {
|
||||
width: 100%;
|
||||
max-width: unset;
|
||||
min-width: unset;
|
||||
|
||||
@@ -603,6 +603,8 @@ ar:
|
||||
share_ai_conversation:
|
||||
name: "مشاركة محادثة الذكاء الاصطناعي"
|
||||
title: "مشاركة هذه المحادثة بشكلٍ علني"
|
||||
invite_ai_conversation:
|
||||
button: "دعوة"
|
||||
ai_label: "الذكاء الاصطناعي"
|
||||
ai_title: "المحادثة مع الذكاء الاصطناعي"
|
||||
share_modal:
|
||||
|
||||
@@ -134,6 +134,8 @@ bg:
|
||||
request: "Заявка"
|
||||
next_log: "Напред"
|
||||
previous_log: "Предишни"
|
||||
invite_ai_conversation:
|
||||
button: "Покана"
|
||||
share_modal:
|
||||
copy: "Копирай"
|
||||
conversations:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user