Compare commits

..
Author SHA1 Message Date
Alan Guo Xiang Tan 9d8895dad1 Revert "DEV: Replace narrowDesktopView logic with viewport API (#1372)"
This reverts commit 59d6e2b467.
2025-05-30 09:51:46 +08:00
David Taylor 59d6e2b467 DEV: Replace narrowDesktopView logic with viewport API (#1372) 2025-05-30 06:52:39 +10:00
SamandKeegan George c06d7b07d5 FEATURE: simplify streaming implementation - rush last update (#1380)
* FEATURE: simplify streaming implementation - rush last update

Previous to this change we would simply "flash" the final update
on the screen, this amends it so we quickly update the UI in about
1 second in the end with all the final update.

This makes the UI feel more interactive to end users.

* DEV: Updates...

- Remove unnecessary comments
- Add doc style comments for all methods
- Organize methods (private at bottom)
- Update some variable names

---------

Co-authored-by: Keegan George <[email protected]>
2025-05-30 06:50:12 +10:00
Natalie Tay 373e2305d6 FEATURE: Automatic translation and localization of posts, topics, categories (#1376)
Related: https://github.com/discourse/discourse-translator/pull/310

This commit includes all the jobs and event hooks to localize posts, topics, and categories.

A few notes:
- `feature_name: "translation"` because the site setting is `ai-translation` and module is `Translation`
- we will switch to proper ai-feature in the near future, and can consider using the persona_user as `localization.localizer_user_id`
- keeping things flat within the module for now as we will be moving to ai-feature soon and have to rearrange
- Settings renamed/introduced are:
  - ai_translation_backfill_rate (0)
  - ai_translation_backfill_limit_to_public_content (true)
  - ai_translation_backfill_max_age_days (5)
  - ai_translation_verbose_logs (false)
2025-05-29 17:28:06 +08:00
Guhyoun Nam ad5c48d9ae DEV: Add appEvents trigger for AI New Question button (#1379)
* DEV: Add appEvents trigger for AI New Question button

* appEvent name update
2025-05-28 13:26:37 -05:00
Keegan George d99c335dab DEV: Ensure enabling/disabling spam is set and logged (#1378)
Since we enable/disable `ai_spam_detection_enabled` setting in a custom Spam tab UI in AI, we want to ensure we retain the setting and logging features. To preserve that, we want to update the controller to use `SiteSetting.set_and_log` instead of setting the value directly.
2025-05-28 10:12:21 -07:00
Roman Rizzi 01e29ca5d8 FIX: Bump persona's examples length (#1377) 2025-05-28 14:01:44 -03:00
Keegan George 297c64c3b8 DEV: Unhide spam detection setting (#1374)
## 🔍 Overview
We want to unhide `ai_spam_detection_enabled` setting so that we can retain staff action log features. However, we also want to ensure users cannot enable spam detection without having `AiModerationSetting.spam` present in the database.

In this update we unhide the setting, but also add a validator to ensure the necessary configuration is in place before allowing the setting to be enabled.
2025-05-28 07:50:56 -07:00
David Taylor ca78b1a1c5 DEV: Patch Net::BufferedIO to help debug spec flakes (#1375)
Internal `/t/154170`
2025-05-28 10:24:07 +01:00
Discourse Translator Bot 39653aed22 Update translations (#1371) 2025-05-27 22:11:32 +02:00
Sam 6781ecd02e DEV: cleanup diff streaming (#1370)
This simplifies some of the internal logic and ensures it is a
bit more robust
2025-05-27 18:12:02 +10:00
Keegan George e264572597 FIX: Closing AI menu stops post audio from playing (#1369)
## 🔍 Overview
When you have a post with audio being played and you open and close the AI post helper menu, it re-renders the entire post DOM, causing the audio to be interrupted and stop playing.

The reason for this is because we highlight the selected text when opening the AI post helper menu and we replace the entire post back with the original post HTML when closing the menu. This fix ensures that we do not re-render the entire post DOM and instead only remove the highlighted section that was added.

## 🔗 Context
https://meta.discourse.org/t/ai-helper-interrupting-uploaded-mp3-audio-stream/366817?u=keegan
2025-05-26 10:24:06 -07:00
Sam 70b0db2871 FIX: improve researcher tool - fix topic filters (#1368)
* Small fix, reasoning is now available on Claude 4 models

* fix invalid filters should raise, topic filter not working

* fix spec so we are consistent
2025-05-26 16:00:44 +10:00
Sam 2d6ec5e1e6 FIX: apply diffs more consistently (#1367)
* FIX: apply diffs more consistently

1. Do escaping direct in diff streamer, that way HTML tags and other unsafe chars can be displayed and fixed
2. Add safeguard to ensure streaming always stops when it was terminated elsewhere

* lint

* bug unsubscribe should unsubscribe
2025-05-24 15:19:48 +10:00
Mark VanLandingham cead887480 FIX: Don't error when navigating from AI Bot topic to regular (#1366)
We were getting an error in this logic causing Ember to fail to render the non-bot-topic that we navigate to.

I believe this is because the getter of participants is re-calculating (due to this.header.topicInfo being updated) before the args to this connector changes. Adding some safe navigation here fixes the issue.
2025-05-23 13:30:08 -05:00
Jordan Vidrine adc92e1e75 UX: Allow rounded corners (#1365) 2025-05-23 10:42:55 -05:00
Roman Rizzi 0ce17a122f FIX: Correctly pass tool_choice when using Claude models. (#1364)
The `ClaudePrompt` object couldn't access the original prompt's tool_choice attribute, affecting both Anthropic and Bedrock.
2025-05-23 10:36:52 -03:00
112 changed files with 3446 additions and 245 deletions
@@ -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)
+51
View File
@@ -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
+51
View File
@@ -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
+68
View File
@@ -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
+63
View File
@@ -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
@@ -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|
>
@@ -18,14 +18,16 @@ 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 selectedText = escapeExpression(this.args.model.selectedText);
@tracked diffStreamer = new DiffStreamer(this.selectedText);
@tracked escapedSelectedText = escapeExpression(this.args.model.selectedText);
@tracked diffStreamer = new DiffStreamer(this.args.model.selectedText);
@tracked suggestion = "";
@tracked
smoothStreamer = new SmoothStreamer(
@@ -45,11 +47,13 @@ export default class ModalDiffModal extends Component {
// Prevents flash by showing the
// original text when the diff is empty
return this.selectedText;
return this.escapedSelectedText;
}
get isStreaming() {
return this.diffStreamer.isStreaming || this.smoothStreamer.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() {
@@ -64,25 +68,23 @@ 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) {
updateResult(result) {
this.loading = false;
if (result.done) {
this.finalResult = result.result;
}
if (result.done) {
this.loading = false;
}
@@ -105,7 +107,7 @@ export default class ModalDiffModal extends Component {
data: {
location: "composer",
mode: this.args.model.mode,
text: this.selectedText,
text: this.args.model.selectedText,
custom_prompt: this.args.model.customPromptValue,
force_default_locale: true,
client_id: this.messageBus.clientId,
@@ -122,7 +124,7 @@ export default class ModalDiffModal extends Component {
if (this.suggestion) {
this.args.model.toolbarEvent.replaceText(
this.selectedText,
this.args.model.selectedText,
this.suggestion
);
}
@@ -131,8 +133,12 @@ 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.selectedText, finalResult);
this.args.model.toolbarEvent.replaceText(
this.args.model.selectedText,
finalResult
);
}
}
@@ -145,7 +151,7 @@ export default class ModalDiffModal extends Component {
<:body>
<div
{{didInsert this.subscribe}}
{{willDestroy this.unsubscribe}}
{{willDestroy this.cleanup}}
class="text-preview"
>
{{#if this.loading}}
@@ -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;
}
@@ -18,8 +18,8 @@ export default class AiConversationInvite extends Component {
get participants() {
const participants = [
...this.header.topicInfo.details.allowed_users,
...this.header.topicInfo.details.allowed_groups,
...(this.header.topicInfo.details?.allowed_users ?? []),
...(this.header.topicInfo.details?.allowed_groups ?? []),
];
return participants;
}
File diff suppressed because it is too large Load Diff
@@ -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,
});
});
}
}
);
});
@@ -1,3 +1,5 @@
@use "lib/viewport";
.composer-ai-helper-modal {
.text-preview,
.inline-diff {
@@ -179,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%,
@@ -189,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%,
@@ -205,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%,
@@ -221,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);
}
}
@@ -232,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 {
@@ -639,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;
+2
View File
@@ -603,6 +603,8 @@ ar:
share_ai_conversation:
name: "مشاركة محادثة الذكاء الاصطناعي"
title: "مشاركة هذه المحادثة بشكلٍ علني"
invite_ai_conversation:
button: "دعوة"
ai_label: "الذكاء الاصطناعي"
ai_title: "المحادثة مع الذكاء الاصطناعي"
share_modal:
+2
View File
@@ -134,6 +134,8 @@ bg:
request: "Заявка"
next_log: "Напред"
previous_log: "Предишни"
invite_ai_conversation:
button: "Покана"
share_modal:
copy: "Копирай"
conversations:
+2
View File
@@ -128,6 +128,8 @@ bs_BA:
response: "Odgovor"
next_log: "Iduće"
previous_log: "Previous"
invite_ai_conversation:
button: "Invite"
share_modal:
copy: "Copy"
conversations:
+2
View File
@@ -133,6 +133,8 @@ ca:
response: "Reacció"
next_log: "Següent"
previous_log: "Previ"
invite_ai_conversation:
button: "Convida"
share_modal:
copy: "Còpia"
conversations:
+45
View File
@@ -211,24 +211,69 @@ cs:
ai_artifact:
collapse_view_label: "Ukončení celé obrazovky (ESC nebo tlačítko Zpět)"
ai_bot:
pm_warning: "Zprávy chatbotů s umělou inteligencí jsou pravidelně monitorovány moderátory."
cancel_streaming: "Zastavit odpověď"
default_pm_prefix: "[Nepojmenovaná SZ od AI bota]"
shortcut_title: "Začněte SZ s AI botem"
share: "Kopírovat konverzaci s AI"
conversation_shared: "Konverzace zkopírována"
debug_ai: "Zobrazit nezpracovaný požadavek a odpověď umělé inteligence"
sidebar_empty: "Zde se zobrazí historie konverzace s botem."
debug_ai_modal:
title: "Zobrazit interakci s umělou inteligencí"
copy_request: "Kopírovat požadavek"
copy_response: "Kopírovat odpověď"
request_tokens: "Tokeny požadavku:"
response_tokens: "Tokeny odpovědi:"
request: "Požadavek"
response: "Odpověď"
next_log: "Další"
previous_log: "Předchozí"
share_full_topic_modal:
title: "Veřejně sdílet konverzaci"
share: "Sdílet a kopírovat odkaz"
update: "Aktualizovat a zkopírovat odkaz"
delete: "Odstranit sdílení"
share_ai_conversation:
name: "Sdílet konverzaci s AI"
title: "Sdílejte veřejně tuto konverzaci s umělou inteligencí"
invite_ai_conversation:
button: "Pozvat"
title: "Pozvat do konverzace s AI"
ai_title: "Konverzace s AI"
share_modal:
title: "Kopírovat konverzaci s AI"
copy: "Kopírovat"
context: "Interakce ke sdílení:"
share_tip: "Můžete také sdílet celou konverzaci."
bot_names:
fake: "Falešný testovací bot"
claude-3-opus: "Claude 3 Opus"
claude-3-sonnet: "Claude 3 Sonet"
claude-3-haiku: "Haiku Claude 3"
cohere-command-r-plus: "Cohere Command R Plus"
gpt-4: "GPT-4"
gpt-4-turbo: "GPT-4 Turbo"
gpt-4o: "GPT-4 Omni"
gpt-3:
5-turbo: "GPT-3.5"
claude-2: "Claude 2"
gemini-1:
5-pro: "Gemini"
mixtral-8x7B-Instruct-V0:
"1": "Mixtral-8x7B V0.1"
conversations:
header: "S čím mohu pomoci?"
submit: "Odeslat otázku"
disclaimer: "Generativní umělá inteligence může dělat chyby. Ověřte si důležité informace."
placeholder: "Zeptejte se..."
new: "Nová otázka"
min_input_length_message:
one: "Zpráva musí mít 1 nebo více znaků"
few: "Zpráva musí mít %{count} nebo více znaků"
many: "Zpráva musí mít %{count} nebo více znaků"
other: "Zpráva musí mít %{count} nebo více znaků"
messages_sidebar_title: "Konverzace"
today: "Dnes"
last_7_days: "Posledních 7 dní"
last_30_days: "Posledních 30 dní"
+2
View File
@@ -147,6 +147,8 @@ da:
response: "Svar"
next_log: "Næste"
previous_log: "Forrige"
invite_ai_conversation:
button: "Invitér"
share_modal:
copy: "Kopier"
conversations:
+8 -1
View File
@@ -328,9 +328,11 @@ de:
key_title: "Schlüssel"
examples:
title: Beispiele
examples_help: Simuliere frühere Interaktionen mit dem LLM und erde es, um bessere Ergebnisse zu erzielen.
new: Neues Beispiel
remove: Beispiel löschen
collapsable_title: "Beispiel #%{number}"
user: "Nachricht des Benutzers"
model: "Antwort des Modells"
list:
enabled: "KI-Bot?"
@@ -522,7 +524,8 @@ de:
provider_quantizations: "Reihenfolge der Provider-Quantisierungen (kommagetrennte Liste, z. B.: fp16,fp8)"
disable_streaming: "Streaming-Vervollständigung deaktivieren (Streaming-Anfragen in Nicht-Streaming-Anfragen umwandeln)"
reasoning_effort: "Argumentationsaufwand (gilt nur für Argumentationsmodelle)"
enable_reasoning: "Argumentation aktivieren (gilt nur für Sonnet 3.7)"
enable_reasoning: "Argumentation aktivieren (nur für Argumentationsmodelle anwendbar)"
enable_thinking: "Denken aktivieren (nur bei entsprechenden Modellen, z.B.: Flash 2.5)"
thinking_tokens: "Anzahl der zum Denken verwendeten Token"
reasoning_tokens: "Anzahl der für die Argumentation verwendeten Token"
disable_temperature: "Temperatur deaktivieren (einige Denkmodelle unterstützen keine Temperatur)"
@@ -664,6 +667,7 @@ de:
share: "KI-Unterhaltung kopieren"
conversation_shared: "Unterhaltung kopiert"
debug_ai: "Rohdaten der KI-Anfrage und -Antwort anzeigen"
sidebar_empty: "Der Verlauf der Bot-Konversation wird hier angezeigt."
debug_ai_modal:
title: "KI-Interaktion ansehen"
copy_request: "Anfrage kopieren"
@@ -682,6 +686,9 @@ de:
share_ai_conversation:
name: "KI-Unterhaltung teilen"
title: "Diese KI-Unterhaltung öffentlich teilen"
invite_ai_conversation:
button: "Einladen"
title: "Zur KI-Konversation einladen"
ai_label: "KI"
ai_title: "Unterhaltung mit KI"
share_modal:
+2
View File
@@ -142,6 +142,8 @@ el:
response: "Απάντηση"
next_log: "Επόμενο"
previous_log: "Προηγούμενο"
invite_ai_conversation:
button: "Πρόσκληση"
share_modal:
copy: "Αντιγραφή"
conversations:
+1 -1
View File
@@ -540,7 +540,7 @@ en:
provider_quantizations: "Order of provider quantizations (comma delimited list eg: fp16,fp8)"
disable_streaming: "Disable streaming completions (convert streaming to non streaming requests)"
reasoning_effort: "Reasoning effort (only applicable to reasoning models)"
enable_reasoning: "Enable reasoning (only applicable to Sonnet 3.7)"
enable_reasoning: "Enable reasoning (only applicable to reasoning models)"
enable_thinking: "Enable thinking (only on applicable models eg: flash 2.5)"
thinking_tokens: "Number of tokens used for thinking"
reasoning_tokens: "Number of tokens used for reasoning"
+2
View File
@@ -591,6 +591,8 @@ es:
share_ai_conversation:
name: "Compartir conversación de IA"
title: "Compartir esta conversación de IA públicamente"
invite_ai_conversation:
button: "Invitar"
ai_label: "IA"
ai_title: "Conversación con IA"
share_modal:
+2
View File
@@ -130,6 +130,8 @@ et:
response: "Vastus"
next_log: "Järgmine"
previous_log: "Eelmine"
invite_ai_conversation:
button: "Kutsu"
share_modal:
copy: "Kopeeri"
conversations:

Some files were not shown because too many files have changed in this diff Show More