Compare commits

..
Author SHA1 Message Date
Sam Saffron 7912b5e8be this code should no longer need 2025-05-23 22:05:17 +10:00
Sam cf220c530c FIX: Improve MessageBus efficiency and correctly stop streaming (#1362)
* FIX: Improve MessageBus efficiency and correctly stop streaming

This commit enhances the message bus implementation for AI helper streaming by:

- Adding client_id targeting for message bus publications to ensure only the requesting client receives streaming updates
- Limiting MessageBus backlog size (2) and age (60 seconds) to prevent Redis bloat
- Replacing clearTimeout with Ember's cancel method for proper runloop management, we were leaking a stop
- Adding tests for client-specific message delivery

These changes improve memory usage and make streaming more reliable by ensuring messages are properly directed to the requesting client.

* composer suggestion needed a fix as well.

* backlog size of 2 is risky here cause same channel name is reused between clients
2025-05-23 16:23:06 +10:00
Keegan George 61ef1932fa DEV: Follow-up small fixes to AI composer helper (#1361)
**This update includes a variety of small fixes to the AI composer helper:**
- ensures there is no flash of no text when diff modal is triggered
- escapes selected text prior to diff streaming
- uses monospace font in diff modal since text rendered is raw markdown
2025-05-22 12:37:42 -07:00
Keegan George 11e90f5f3f DEV: Hide proofreading shortcut in toolbar title (#1360) 2025-05-22 09:36:55 -07:00
Joffrey JAFFEUX b817810d46 FIX: ensures stream update object is scoped to its initial topic (#1359)
* FIX: ensures stream update object is scoped to its initial topic

Before this commit you could end up in this situation where a `post-updater` is constructed for a specific topic, but the user changes topic mid steam and it ends up updating the same post number but in a  different topic as we were only checking for `post_number` and not the combination of `topic_id` + `post_number`.

* we should have the topic
2025-05-22 20:08:43 +10:00
Sam 55dab9c68a FIX: stabilize diff algorithm for streaming (#1358)
Previous to this fix we would diff the entire body of text, this
could lead to situations where a diff presented to a user was
wildly off matching areas in the text that should not have been
tested.

New algorithm only checks a portion of the string, this ensures
that during streaming there is no chance for wild mistakes
2025-05-22 16:31:21 +10:00
Keegan GeorgeandSam Saffron c29183fc2d FEATURE: Add diff streaming animation (#1355)
Previously we attempted to add a diff streaming animation to the AI composer helper: https://github.com/discourse/discourse-ai/pull/1332, but it resulted in issues despite attempted fixes (https://github.com/discourse/discourse-ai/pull/1338) so we temporarily suppressed the diff animation (https://github.com/discourse/discourse-ai/pull/1341).

This update makes a second attempt at implementing the diff streaming animation. Instead of creating a custom diff algorithm, we make use of a third-party library [`jsDiff`](https://github.com/kpdecker/jsdiff) (which we added to core here: https://github.com/discourse/discourse/pull/32833). While streaming, the diff animation often struggles with markdown links and images, so we make use of `markdown-it` parser to detect those cases and prevent breaking the animation.

---------

Co-authored-by: Sam Saffron <[email protected]>
2025-05-22 08:10:50 +10:00
Roman Rizzi d72ad84f8f FIX: Retry parsing escaped inner JSON to handle control chars. (#1357)
The structured output JSON comes embedded inside the API response, which is also a JSON. Since we have to parse the response to process it, any control characters inside the structured output are unescaped into regular characters, leading to invalid JSON and breaking during parsing. This change adds a retry mechanism that escapes
the string again if parsing fails, preventing the parser from breaking on malformed input and working around this issue.

For example:

```
  original = '{ "a": "{\\"key\\":\\"value with \\n newline\\"}" }'
  JSON.parse(original) => { "a" => "{\"key\":\"value with \n newline\"}" }
  # At this point, the inner JSON string contains an actual newline.
```
2025-05-21 11:25:59 -03:00
Roman Rizzi e207eba1a4 FIX: Don't dig on nil when checking for the gemini schema (#1356) 2025-05-21 08:30:47 -03:00
Kris 53905f65ac FEATURE: add participants and invite button to AI conversations (#1354) 2025-05-21 10:27:06 +10:00
Guhyoun Nam ef260be7cf DEV: Move the stop-forcing-conversations-sidebar appEvents trigger (#1353) 2025-05-20 15:06:54 -05:00
Discourse Translator Bot 0c69bb85ce Update translations (#1352) 2025-05-20 17:33:52 +02:00
Sam af18603b21 DEV: cancel manager should bypass webmock (#1350)
Webmock can be a bit flaky under certain use cases.
2025-05-20 13:01:55 +10:00
Sam 7db2589cc4 DEV: prompt engineering to improve citations (#1351) 2025-05-20 13:01:35 +10:00
Roman Rizzi 2fb691cba8 FEATURE: Triage can hide posts after adding them to the review queue (#1348) 2025-05-20 08:19:00 +10:00
Sam de0625571b FIX: add safe navigation to serializer include conditions (#1349)
In some rare cases a post can exist without a topic
2025-05-19 18:55:55 -03:00
Joffrey JAFFEUX 296aa24df1 DEV: rewrites artifact spec with capybara waiters (#1347)
Generally speaking we never want to do:

```
expect(element.text).to eq("foo")
```

As these are rspec matchers and do not add further Capybara-style waiting specifically for the text content to become present.
2025-05-20 07:27:15 +10:00
52 changed files with 873 additions and 286 deletions
@@ -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
@@ -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
@@ -1,29 +0,0 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import { service } from "@ember/service";
import DButton from "discourse/components/d-button";
import AddPmParticipants from "discourse/components/modal/add-pm-participants";
export default class AiConversationsInvite extends Component {
@service site;
@service modal;
@action
showInvite() {
this.modal.show(AddPmParticipants, {
model: {
title: "discourse_ai.ai_bot.invite_ai_conversation.title",
inviteModel: this.args.topic,
},
});
}
<template>
<DButton
@icon="user-plus"
@label="discourse_ai.ai_bot.invite_ai_conversation.button"
@action={{this.showInvite}}
class="ai-conversations__invite-button"
/>
</template>
}
@@ -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,6 +12,7 @@ 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";
@@ -23,21 +24,34 @@ export default class ModalDiffModal extends Component {
@tracked loading = false;
@tracked finalResult = "";
@tracked showcasedDiff = "";
@tracked diffStreamer = new DiffStreamer(this.args.model.selectedText);
@tracked selectedText = escapeExpression(this.args.model.selectedText);
@tracked diffStreamer = new DiffStreamer(this.selectedText);
@tracked suggestion = "";
@tracked
smoothStreamer = new SmoothStreamer(
() => 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.selectedText;
}
get isStreaming() {
return this.diffStreamer.isStreaming || this.smoothStreamer.isStreaming;
}
get primaryBtnLabel() {
return this.loading
? i18n("discourse_ai.ai_helper.context_menu.loading")
@@ -62,23 +76,14 @@ export default class ModalDiffModal extends Component {
@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.
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) {
@@ -100,9 +105,10 @@ export default class ModalDiffModal extends Component {
data: {
location: "composer",
mode: this.args.model.mode,
text: this.args.model.selectedText,
text: this.selectedText,
custom_prompt: this.args.model.customPromptValue,
force_default_locale: true,
client_id: this.messageBus.clientId,
},
});
} catch (e) {
@@ -116,7 +122,7 @@ export default class ModalDiffModal extends Component {
if (this.suggestion) {
this.args.model.toolbarEvent.replaceText(
this.args.model.selectedText,
this.selectedText,
this.suggestion
);
}
@@ -126,10 +132,7 @@ export default class ModalDiffModal extends Component {
? this.finalResult
: this.diffStreamer.suggestion;
if (this.args.model.showResultAsDiff && finalResult) {
this.args.model.toolbarEvent.replaceText(
this.args.model.selectedText,
finalResult
);
this.args.model.toolbarEvent.replaceText(this.selectedText, finalResult);
}
}
@@ -140,10 +143,14 @@ export default class ModalDiffModal extends Component {
@closeModal={{@closeModal}}
>
<:body>
<div {{didInsert this.subscribe}} {{willDestroy this.unsubscribe}}>
<div
{{didInsert this.subscribe}}
{{willDestroy this.unsubscribe}}
class="text-preview"
>
{{#if this.loading}}
<div class="composer-ai-helper-modal__loading">
<CookText @rawText={{@model.selectedText}} />
{{~@model.selectedText~}}
</div>
{{else}}
<div
@@ -152,10 +159,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
@@ -1,10 +0,0 @@
import Component from "@glimmer/component";
import AiConversationsInvite from "../../components/ai-conversations-invite";
export default class AiConversationsInviteConnector extends Component {
static shouldRender(args) {
return args.topic.is_bot_pm;
}
<template><AiConversationsInvite @topic={{@outletArgs.topic}} /></template>
}
@@ -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>
}
@@ -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) {
+36 -2
View File
@@ -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;
}
@@ -450,10 +449,87 @@ body.has-ai-conversations-sidebar {
button:not(
.create,
.share-ai-conversation-button,
.topic-admin-menu-trigger,
.ai-conversations__invite-button
.topic-admin-menu-trigger
) {
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,9 @@
.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;
+13
View File
@@ -320,9 +320,18 @@ de:
rag_conversation_chunks_help: "Die Anzahl der Chunks, die für die RAG-Modell-Suche verwendet werden. Erhöhen, um die Menge des Kontexts zu steigern, den die KI verwenden kann."
persona_description: "Personas sind eine leistungsstarke Funktion, mit der du das Verhalten der KI-Engine in deinem Discourse-Forum anpassen kannst. Sie fungieren als „Systemnachricht“, welche die Antworten und Interaktionen der KI steuert und dazu beiträgt, ein persönlicheres und ansprechenderes Erlebnis für Benutzer zu schaffen."
response_format:
title: "JSON-Antwortformat"
no_format: "Kein JSON-Format angegeben"
open_modal: "Bearbeiten"
modal:
root_title: "Antwortstruktur"
key_title: "Schlüssel"
examples:
title: Beispiele
new: Neues Beispiel
remove: Beispiel löschen
collapsable_title: "Beispiel #%{number}"
model: "Antwort des Modells"
list:
enabled: "KI-Bot?"
ai_bot:
@@ -514,6 +523,7 @@ de:
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)"
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)"
disable_top_p: "Top P deaktivieren (einige Denkmodelle unterstützen Top P nicht)"
@@ -701,6 +711,9 @@ de:
disclaimer: "Generative KI kann Fehler machen. Überprüfe wichtige Informationen."
placeholder: "Stell eine Frage ..."
new: "Neue Frage"
min_input_length_message:
one: "Nachricht muss mindestens 1 Zeichen lang sein"
other: "Nachricht muss mindestens %{count} Zeichen lang sein"
messages_sidebar_title: "Unterhaltungen"
today: "Heute"
last_7_days: "Letzte 7 Tage"
+1 -1
View File
@@ -709,7 +709,7 @@ en:
delete: "Delete share"
share_ai_conversation:
name: "Share"
name: "Share AI conversation"
title: "Share this AI conversation publicly"
invite_ai_conversation:
+8
View File
@@ -309,6 +309,13 @@ he:
modal:
root_title: "מבנה תגובה"
key_title: "מפתח"
examples:
title: דוגמאות
new: דוגמה חדשה
remove: מחיקת דוגמה
collapsable_title: "דוגמה מס׳ %{number}"
user: "הודעת משתמש"
model: "תגובת המודל"
list:
enabled: "בוט בינה מלאכותית?"
ai_bot:
@@ -714,6 +721,7 @@ he:
discobot_discoveries:
regular_results: "נושאים"
tell_me_more: "אשמח לדעת עוד…"
continue_convo: "המשך בשיחה…"
collapse: "צמצום"
tooltip:
header: "חיפוש מופעל בינה מלאכותית"
+3 -3
View File
@@ -261,9 +261,6 @@ ar:
settings_explorer:
name: مستكشف الإعدادات
description: "روبوت ذكاء اصطناعي متخصص في المساعدة في استكشاف إعدادات موقع Discourse"
researcher:
name: الباحث
description: "روبوت ذكاء اصطناعي مزوَّد بإمكانية الوصول إلى Google، ويمكنه البحث عن المعلومات نيابةً عنك"
creative:
name: المبدع
description: "روبوت ذكاء اصطناعي من دون تكاملات خارجية متخصصة في المهام الإبداعية"
@@ -280,6 +277,9 @@ ar:
summarizing: "جارٍ تلخيص الموضوع"
searching: "جارٍ البحث عن: '%{query}'"
tool_options:
researcher:
max_results:
name: "الحد الأقصى لعدد النتائج"
google:
base_query:
name: "استعلام البحث الأساسي"
+28 -2
View File
@@ -106,6 +106,7 @@ de:
ai_discord_search_mode: "Wähle den Suchmodus für die Discord-Suche aus"
ai_discord_search_persona: "Die Persona, die für die Discord-Suche verwendet wird."
ai_discord_allowed_guilds: "Discord-Gilden (Server), auf denen der Bot suchen darf"
ai_bot_enable_dedicated_ux: "Erlaube eine Vollbild-Bot-Oberfläche anstelle einer PM"
reviewables:
reasons:
flagged_by_toxicity: Das KI-Plug-in meldete dies nach der Klassifizierung als toxisch.
@@ -278,6 +279,11 @@ de:
artist:
name: Künstler
description: "KI-Bot, spezialisiert auf die Erstellung von Bildern"
designer:
name: Designer
forum_researcher:
name: Forum-Rechercheur
description: "KI-Bot, spezialisiert auf gründliche Recherche für das Forum"
sql_helper:
name: SQL-Helfer
description: "KI-Bot, der darauf spezialisiert ist, bei der Erstellung von SQL-Abfragen auf dieser Discourse-Instanz zu helfen"
@@ -285,8 +291,8 @@ de:
name: Einstellungs-Explorer
description: "KI-Bot, der darauf spezialisiert ist, die Einstellungen der Discourse-Website zu erkunden"
researcher:
name: Forscher
description: "KI-Bot mit Google-Zugang, der Informationen für dich recherchieren kann"
name: Web-Rechercheur
description: "KI-Bot mit Google-Zugriff, der Webseiten sowohl suchen als auch lesen kann"
creative:
name: Kreativ
description: "KI-Bot ohne externe Integrationen, spezialisiert auf kreative Aufgaben"
@@ -309,6 +315,16 @@ de:
summarizing: "Thema zusammenfassen"
searching: "Suche nach: „%{query}“"
tool_options:
researcher:
max_results:
name: "Maximale Anzahl von Ergebnissen"
description: "Maximale Anzahl von Ergebnissen, die in einen Filter aufgenommen werden"
include_private:
name: "Private einschließen"
description: "Private Themen in die Filter einbeziehen"
max_tokens_per_post:
name: "Maximale Token pro Beitrag"
description: "Maximale Anzahl von Token, die für jeden Beitrag im Filter verwendet werden"
create_artifact:
creator_llm:
name: "LLM"
@@ -365,6 +381,7 @@ de:
dall_e: "Bild generieren"
search_meta_discourse: "Meta-Discourse durchsuchen"
javascript_evaluator: "JavaScript auswerten"
researcher: "Rechercheur"
tool_help:
read_artifact: "Lesen eines Webartefakts mit dem KI-Bot"
update_artifact: "Aktualisiere ein Web-Artefakt mit dem KI-Bot"
@@ -389,6 +406,7 @@ de:
dall_e: "Bild mit DALL-E 3 generieren"
search_meta_discourse: "Meta-Discourse durchsuchen"
javascript_evaluator: "JavaScript auswerten"
researcher: "Foreninformationen mit dem KI-Bot recherchieren"
tool_description:
read_artifact: "Lesen eines Webartefakts mit dem KI-Bot"
update_artifact: "Ein Web-Artefakt wurde mit dem KI-Bot aktualisiert"
@@ -423,6 +441,12 @@ de:
other: "%{count} <a href='%{url}'>Ergebnisse</a> für „%{query}“ gefunden"
setting_context: "Kontext wird gelesen für: %{setting_name}"
schema: "%{tables}"
researcher_dry_run:
one: "Vorgeschlagene Ziele: %{goals}\n\n%{count} Beitrag passend zu „%{filter}“ gefunden"
other: "Vorgeschlagene Ziele: %{goals}\n\n%{count} Beiträge passend zu „%{filter}“ gefunden"
researcher:
one: "Recherche: %{goals}\n\n%{count} Beitrag passend zu „%{filter}“ gefunden"
other: "Recherche: %{goals}\n\n%{count} Beiträge passend zu „%{filter}“ gefunden"
search_settings:
one: "%{count} Ergebnis für „%{query}“ gefunden"
other: "%{count} Ergebnisse für „%{query}“ gefunden"
@@ -468,6 +492,8 @@ de:
one: "Wir konnten dieses Modell nicht löschen, weil es von %{settings} verwendet wird. Aktualisiere die Einstellung und versuche es erneut."
other: "Wir konnten dieses Modell nicht löschen, weil %{settings} es verwenden. Aktualisiere die Einstellungen und versuche es erneut."
cannot_edit_builtin: "Du kannst ein integriertes Modell nicht bearbeiten."
personas:
malformed_examples: "Die angegebenen Beispiele haben das falsche Format."
embeddings:
delete_failed: "Dieses Modell wird derzeit verwendet. Aktualisiere zuerst `ai embeddings selected model`."
cannot_edit_builtin: "Du kannst ein integriertes Modell nicht bearbeiten."
+1
View File
@@ -3,6 +3,7 @@ en:
ai:
flag_types:
review: "Add post to review queue"
review_hide: "Add post to review queue and hide post"
spam: "Flag as spam and hide post"
spam_silence: "Flag as spam, hide post and silence user"
scriptables:
+3 -3
View File
@@ -262,9 +262,6 @@ es:
settings_explorer:
name: Explorador de ajustes
description: "Bot de IA especializado en ayudar a explorar los ajustes del sitio Discourse"
researcher:
name: Investigador
description: "Bot de IA con acceso a Google que puede buscar información por ti"
creative:
name: Creativa
description: "Bot de IA sin integraciones externas especializado en tareas creativas"
@@ -281,6 +278,9 @@ es:
summarizing: "Resumiendo tema"
searching: "Buscando: '%{query}'"
tool_options:
researcher:
max_results:
name: "Número máximo de resultados"
google:
base_query:
name: "Consulta de búsqueda básica"
+3 -3
View File
@@ -262,9 +262,6 @@ fi:
settings_explorer:
name: Asetusapuri
description: "Tekoälybotti, joka on erikoistunut auttamaan Discoursen sivustoasetuksiin tutustumisessa"
researcher:
name: Tutkija
description: "Tekoälybotti, joka voi käyttää Googlea ja etsiä tietoja puolestasi"
creative:
name: Luova
description: "Tekoälybotti ilman ulkoisia integraatioita, joka on erikoistuneet luoviin tehtäviin"
@@ -281,6 +278,9 @@ fi:
summarizing: "Laaditaan yhteenvetoa ketjusta"
searching: "Haetaan: \"%{query}\""
tool_options:
researcher:
max_results:
name: "Tulosten enimmäismäärä"
google:
base_query:
name: "Perushakukysely"
+3 -3
View File
@@ -262,9 +262,6 @@ fr:
settings_explorer:
name: Explorateur de paramètres
description: "Bot IA spécialisé dans l'aide à l'exploration des paramètres du site Discourse"
researcher:
name: Chercheur
description: "Bot IA avec accès Google qui peut rechercher des informations pour vous"
creative:
name: Créatif
description: "Bot IA sans intégration externe spécialisé dans les tâches créatives"
@@ -281,6 +278,9 @@ fr:
summarizing: "Synthèse du sujet"
searching: "Recherche de : '%{query}'"
tool_options:
researcher:
max_results:
name: "Nombre maximal de résultats"
google:
base_query:
name: "Requête de recherche de base"
+14 -3
View File
@@ -283,9 +283,6 @@ he:
settings_explorer:
name: סייר הגדרות
description: "בוט בינה מלאכותית שמתמחה בסיור בהגדרות אתר ה־Discourse"
researcher:
name: מחקר
description: "בוט בינה מלאכותית עם גישה ל־Google שיכול לגשת למידע מחקרי בשבילך"
creative:
name: יצירה
description: "בוט בינה מלאכותית ללא שילובים חיצוניים שמתמחה ביצירת משימות יצירתיות"
@@ -306,6 +303,9 @@ he:
summarizing: "הנושא מסוכם"
searching: "חיפוש אחר: ‚%{query}’"
tool_options:
researcher:
max_results:
name: "מספר תוצאות מרבי"
create_artifact:
creator_llm:
name: "LLM"
@@ -363,6 +363,7 @@ he:
javascript_evaluator: "שערוך JavaScript"
create_image: "יצירת תמונה"
edit_image: "עריכת תמונה"
researcher_dry_run: "המחקר בהכנה"
tool_help:
read_artifact: "לקרוא תוצר דפדפני באמצעות בוט הבינה המלאכותית"
update_artifact: "עדכון תוצר דפדפני באמצעות בוט הבינה המלאכותית"
@@ -433,6 +434,16 @@ he:
other: "נמצאו %{count} <a href='%{url}'>תוצאות</a> לחיפוש אחר ‚%{query}’"
setting_context: "נקרא הקשר עבור: %{setting_name}"
schema: "%{tables}"
researcher_dry_run:
one: "יעדים מוצעים: %{goals}\n\nנמצא פוסט שעונה על ‚%{filter}’"
two: "יעדים מוצעים: %{goals}\n\nנמצאו שני פוסטים שעונים על ‚%{filter}’"
many: "יעדים מוצעים: %{goals}\n\nנמצאו %{count} פוסטים שעונים על ‚%{filter}’"
other: "יעדים מוצעים: %{goals}\n\nנמצאו %{count} פוסטים שעונים על ‚%{filter}’"
researcher:
one: "במהלך מחקר: %{goals}\n\nנמצא פוסט שעונה על ‚%{filter}’"
two: "במהלך מחקר: %{goals}\n\nנמצאו שני פוסטים שעונים על ‚%{filter}’"
many: "במהלך מחקר: %{goals}\n\nנמצאו %{count} פוסטים שעונים על ‚%{filter}’"
other: "במהלך מחקר: %{goals}\n\nנמצאו %{count} פוסטים שעונים על ‚%{filter}’"
search_settings:
one: "נמצאה תוצאה לחיפוש אחר ‚%{query}’"
two: "נמצאו שתי תוצאות לחיפוש אחר ‚%{query}’"
+3 -3
View File
@@ -262,9 +262,6 @@ it:
settings_explorer:
name: Esploratore di impostazioni
description: "Bot IA specializzato nell'aiutare a esplorare le impostazioni del sito Discourse"
researcher:
name: Ricercatore
description: "Bot IA con accesso a Google che può ricercare informazioni per te"
creative:
name: Creativo
description: "Bot IA senza integrazioni esterne specializzato in attività creative"
@@ -281,6 +278,9 @@ it:
summarizing: "Riepilogo argomento"
searching: "Ricerca di: '%{query}'"
tool_options:
researcher:
max_results:
name: "Numero massimo di risultati"
google:
base_query:
name: "Query di ricerca di base"
+3 -3
View File
@@ -260,9 +260,6 @@ ja:
settings_explorer:
name: 設定エクスプローラー
description: "Discourse のサイト設定のナビ支援を専門とする AI ボット"
researcher:
name: リサーチャー
description: "ユーザーに代わって Google にアクセスして情報を調査できる AI ボット"
creative:
name: クリエイティブ
description: "外部統合のないクリエイティブ系タスクを専門とする AI ボット"
@@ -279,6 +276,9 @@ ja:
summarizing: "トピックの要約を生成中"
searching: "検索中: '%{query}'"
tool_options:
researcher:
max_results:
name: "結果の最大件数"
google:
base_query:
name: "ベース検索クエリ"
+3 -3
View File
@@ -262,9 +262,6 @@ nl:
settings_explorer:
name: Instellingenverkenner
description: "AI-bot gespecialiseerd in het verkennen van Discourse-site-instellingen"
researcher:
name: Onderzoeker
description: "AI-bot met toegang tot Google die informatie voor je kan onderzoeken"
creative:
name: Creatief
description: "AI-bot zonder externe integraties, gespecialiseerd in creatieve taken"
@@ -281,6 +278,9 @@ nl:
summarizing: "Topic samenvatten"
searching: "Zoeken naar: '%{query}'"
tool_options:
researcher:
max_results:
name: "Maximaal aantal resultaten"
google:
base_query:
name: "Basiszoekquery"
+3 -3
View File
@@ -141,9 +141,6 @@ pl_PL:
settings_explorer:
name: Eksplorator ustawień
description: "Bot AI specjalizujący się w eksploracji ustawień strony Discourse"
researcher:
name: Badacz
description: "Bot AI z dostępem do Google, który może wyszukiwać informacje za Ciebie"
creative:
name: Kreatywny
description: "Bot AI bez zewnętrznych integracji specjalizujący się w zadaniach kreatywnych"
@@ -156,6 +153,9 @@ pl_PL:
summarizing: "Podsumowanie tematu"
searching: "Wyszukiwanie: '%{query}'"
tool_options:
researcher:
max_results:
name: "Maksymalna liczba wyników"
update_artifact:
update_algorithm:
description: "Poproś LLM o pełne zastąpienie lub użyj diff do aktualizacji"
+3 -3
View File
@@ -261,9 +261,6 @@ pt_BR:
settings_explorer:
name: Explorador de configurações
description: "Bot de IA especializado em ajudar a explorar as configurações do site do Discourse"
researcher:
name: Pesquisador(a)
description: "Esta informação pode ser pesquisada com um Bot de IA com acesso ao Google para você"
creative:
name: Criativa
description: "Bot de IA sem integrações externas especializado em tarefas criativas"
@@ -280,6 +277,9 @@ pt_BR:
summarizing: "Resumindo tópico"
searching: "Pesquisando: \"%{query}\""
tool_options:
researcher:
max_results:
name: "O número máximo de resultados"
google:
base_query:
name: "Consulta de pesquisa básica"

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