Compare commits
29
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7912b5e8be | ||
|
|
cf220c530c | ||
|
|
61ef1932fa | ||
|
|
11e90f5f3f | ||
|
|
b817810d46 | ||
|
|
55dab9c68a | ||
|
|
c29183fc2d | ||
|
|
d72ad84f8f | ||
|
|
e207eba1a4 | ||
|
|
53905f65ac | ||
|
|
ef260be7cf | ||
|
|
0c69bb85ce | ||
|
|
af18603b21 | ||
|
|
7db2589cc4 | ||
|
|
2fb691cba8 | ||
|
|
de0625571b | ||
|
|
296aa24df1 | ||
|
|
3ac2359ff1 | ||
|
|
925949de47 | ||
|
|
b2fbab9fad | ||
|
|
381fa14158 | ||
|
|
77d3a38e49 | ||
|
|
7316058dfc | ||
|
|
9ee82fd8be | ||
|
|
dfea784fc4 | ||
|
|
ff2e18f9ca | ||
|
|
1b3fdad5c7 | ||
|
|
2c6459429f | ||
|
|
c34fcc8a95 |
@@ -42,6 +42,21 @@ module DiscourseAi
|
|||||||
<style>
|
<style>
|
||||||
#{artifact.css}
|
#{artifact.css}
|
||||||
</style>
|
</style>
|
||||||
|
<script>
|
||||||
|
window._discourse_user_data = {
|
||||||
|
#{current_user ? "username: #{current_user.username.to_json}" : "username: null"}
|
||||||
|
};
|
||||||
|
window.discourseArtifactReady = new Promise(resolve => {
|
||||||
|
window._resolveArtifactData = resolve;
|
||||||
|
});
|
||||||
|
window.addEventListener('message', function(event) {
|
||||||
|
if (event.data && event.data.type === 'discourse-artifact-data') {
|
||||||
|
window.discourseArtifactData = event.data.dataset || {};
|
||||||
|
Object.assign(window.discourseArtifactData, window._discourse_user_data);
|
||||||
|
window._resolveArtifactData(window.discourseArtifactData);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
#{artifact.html}
|
#{artifact.html}
|
||||||
@@ -74,6 +89,19 @@ module DiscourseAi
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<iframe sandbox="allow-scripts allow-forms" height="100%" width="100%" srcdoc="#{ERB::Util.html_escape(untrusted_html)}" frameborder="0"></iframe>
|
<iframe sandbox="allow-scripts allow-forms" height="100%" width="100%" srcdoc="#{ERB::Util.html_escape(untrusted_html)}" frameborder="0"></iframe>
|
||||||
|
<script>
|
||||||
|
document.querySelector('iframe').addEventListener('load', function() {
|
||||||
|
try {
|
||||||
|
const iframeWindow = this.contentWindow;
|
||||||
|
const message = { type: 'discourse-artifact-data', dataset: {} };
|
||||||
|
|
||||||
|
if (window.frameElement && window.frameElement.dataset) {
|
||||||
|
Object.assign(message.dataset, window.frameElement.dataset);
|
||||||
|
}
|
||||||
|
iframeWindow.postMessage(message, '*');
|
||||||
|
} catch (e) { console.error('Error passing data to artifact:', e); }
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
HTML
|
HTML
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ module DiscourseAi
|
|||||||
before_action :require_site_settings!
|
before_action :require_site_settings!
|
||||||
|
|
||||||
skip_before_action :preload_json, :check_xhr, only: %i[show asset]
|
skip_before_action :preload_json, :check_xhr, only: %i[show asset]
|
||||||
skip_before_action :verify_authenticity_token, only: ["asset"]
|
skip_before_action :redirect_to_login_if_required, :verify_authenticity_token, only: %i[asset]
|
||||||
|
|
||||||
def create
|
def create
|
||||||
ensure_allowed_create!
|
ensure_allowed_create!
|
||||||
|
|||||||
@@ -124,6 +124,10 @@ module DiscourseAi
|
|||||||
raise Discourse::InvalidParameters.new(:custom_prompt) if params[:custom_prompt].blank?
|
raise Discourse::InvalidParameters.new(:custom_prompt) if params[:custom_prompt].blank?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# to stream we must have an appropriate client_id
|
||||||
|
# otherwise we may end up streaming the data to the wrong client
|
||||||
|
raise Discourse::InvalidParameters.new(:client_id) if params[:client_id].blank?
|
||||||
|
|
||||||
if location == "composer"
|
if location == "composer"
|
||||||
Jobs.enqueue(
|
Jobs.enqueue(
|
||||||
:stream_composer_helper,
|
:stream_composer_helper,
|
||||||
@@ -132,6 +136,7 @@ module DiscourseAi
|
|||||||
prompt: prompt.name,
|
prompt: prompt.name,
|
||||||
custom_prompt: params[:custom_prompt],
|
custom_prompt: params[:custom_prompt],
|
||||||
force_default_locale: params[:force_default_locale] || false,
|
force_default_locale: params[:force_default_locale] || false,
|
||||||
|
client_id: params[:client_id],
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
post_id = get_post_param!
|
post_id = get_post_param!
|
||||||
@@ -146,6 +151,7 @@ module DiscourseAi
|
|||||||
text: text,
|
text: text,
|
||||||
prompt: prompt.name,
|
prompt: prompt.name,
|
||||||
custom_prompt: params[:custom_prompt],
|
custom_prompt: params[:custom_prompt],
|
||||||
|
client_id: params[:client_id],
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ module Jobs
|
|||||||
return unless args[:prompt]
|
return unless args[:prompt]
|
||||||
return unless user = User.find_by(id: args[:user_id])
|
return unless user = User.find_by(id: args[:user_id])
|
||||||
return unless args[:text]
|
return unless args[:text]
|
||||||
|
return unless args[:client_id]
|
||||||
|
|
||||||
prompt = CompletionPrompt.enabled_by_name(args[:prompt])
|
prompt = CompletionPrompt.enabled_by_name(args[:prompt])
|
||||||
|
|
||||||
@@ -21,6 +22,7 @@ module Jobs
|
|||||||
user,
|
user,
|
||||||
"/discourse-ai/ai-helper/stream_composer_suggestion",
|
"/discourse-ai/ai-helper/stream_composer_suggestion",
|
||||||
force_default_locale: args[:force_default_locale],
|
force_default_locale: args[:force_default_locale],
|
||||||
|
client_id: args[:client_id],
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -266,6 +266,7 @@ class AiPersona < ActiveRecord::Base
|
|||||||
define_method(:top_p) { @ai_persona&.top_p }
|
define_method(:top_p) { @ai_persona&.top_p }
|
||||||
define_method(:system_prompt) { @ai_persona&.system_prompt || "You are a helpful bot." }
|
define_method(:system_prompt) { @ai_persona&.system_prompt || "You are a helpful bot." }
|
||||||
define_method(:uploads) { @ai_persona&.uploads }
|
define_method(:uploads) { @ai_persona&.uploads }
|
||||||
|
define_method(:response_format) { @ai_persona&.response_format }
|
||||||
define_method(:examples) { @ai_persona&.examples }
|
define_method(:examples) { @ai_persona&.examples }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import Component from "@glimmer/component";
|
import Component from "@glimmer/component";
|
||||||
import { tracked } from "@glimmer/tracking";
|
import { tracked } from "@glimmer/tracking";
|
||||||
import { action } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
|
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
||||||
import { service } from "@ember/service";
|
import { service } from "@ember/service";
|
||||||
import DButton from "discourse/components/d-button";
|
import DButton from "discourse/components/d-button";
|
||||||
import htmlClass from "discourse/helpers/html-class";
|
import htmlClass from "discourse/helpers/html-class";
|
||||||
@@ -88,6 +89,15 @@ export default class AiArtifactComponent extends Component {
|
|||||||
}`;
|
}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
setDataAttributes(element) {
|
||||||
|
if (this.args.dataAttributes) {
|
||||||
|
Object.entries(this.args.dataAttributes).forEach(([key, value]) => {
|
||||||
|
element.setAttribute(key, value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
{{#if this.expanded}}
|
{{#if this.expanded}}
|
||||||
{{htmlClass "ai-artifact-expanded"}}
|
{{htmlClass "ai-artifact-expanded"}}
|
||||||
@@ -118,6 +128,7 @@ export default class AiArtifactComponent extends Component {
|
|||||||
src={{this.artifactUrl}}
|
src={{this.artifactUrl}}
|
||||||
width="100%"
|
width="100%"
|
||||||
frameborder="0"
|
frameborder="0"
|
||||||
|
{{didInsert this.setDataAttributes}}
|
||||||
></iframe>
|
></iframe>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#unless this.requireClickToRun}}
|
{{#unless this.requireClickToRun}}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,7 @@ import { composeAiBotMessage } from "../lib/ai-bot-helper";
|
|||||||
import { AI_CONVERSATIONS_PANEL } from "../services/ai-conversations-sidebar-manager";
|
import { AI_CONVERSATIONS_PANEL } from "../services/ai-conversations-sidebar-manager";
|
||||||
|
|
||||||
export default class AiBotHeaderIcon extends Component {
|
export default class AiBotHeaderIcon extends Component {
|
||||||
|
@service appEvents;
|
||||||
@service composer;
|
@service composer;
|
||||||
@service currentUser;
|
@service currentUser;
|
||||||
@service navigationMenu;
|
@service navigationMenu;
|
||||||
@@ -51,6 +52,7 @@ export default class AiBotHeaderIcon extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.siteSettings.ai_bot_enable_dedicated_ux) {
|
if (this.siteSettings.ai_bot_enable_dedicated_ux) {
|
||||||
|
this.appEvents.trigger("discourse-ai:bot-header-icon-clicked");
|
||||||
return this.router.transitionTo("discourse-ai-bot-conversations");
|
return this.router.transitionTo("discourse-ai-bot-conversations");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -242,6 +242,7 @@ export default class AiPostHelperMenu extends Component {
|
|||||||
text: this.args.data.selectedText,
|
text: this.args.data.selectedText,
|
||||||
post_id: this.args.data.quoteState.postId,
|
post_id: this.args.data.quoteState.postId,
|
||||||
custom_prompt: this.customPromptValue,
|
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 { ajax } from "discourse/lib/ajax";
|
||||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
import { bind } from "discourse/lib/decorators";
|
import { bind } from "discourse/lib/decorators";
|
||||||
|
import { escapeExpression } from "discourse/lib/utilities";
|
||||||
import { i18n } from "discourse-i18n";
|
import { i18n } from "discourse-i18n";
|
||||||
import DiffStreamer from "../../lib/diff-streamer";
|
import DiffStreamer from "../../lib/diff-streamer";
|
||||||
import SmoothStreamer from "../../lib/smooth-streamer";
|
import SmoothStreamer from "../../lib/smooth-streamer";
|
||||||
@@ -22,7 +23,9 @@ export default class ModalDiffModal extends Component {
|
|||||||
@service messageBus;
|
@service messageBus;
|
||||||
|
|
||||||
@tracked loading = false;
|
@tracked loading = false;
|
||||||
@tracked diffStreamer = new DiffStreamer(this.args.model.selectedText);
|
@tracked finalResult = "";
|
||||||
|
@tracked selectedText = escapeExpression(this.args.model.selectedText);
|
||||||
|
@tracked diffStreamer = new DiffStreamer(this.selectedText);
|
||||||
@tracked suggestion = "";
|
@tracked suggestion = "";
|
||||||
@tracked
|
@tracked
|
||||||
smoothStreamer = new SmoothStreamer(
|
smoothStreamer = new SmoothStreamer(
|
||||||
@@ -35,6 +38,16 @@ export default class ModalDiffModal extends Component {
|
|||||||
this.suggestChanges();
|
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() {
|
get isStreaming() {
|
||||||
return this.diffStreamer.isStreaming || this.smoothStreamer.isStreaming;
|
return this.diffStreamer.isStreaming || this.smoothStreamer.isStreaming;
|
||||||
}
|
}
|
||||||
@@ -65,6 +78,14 @@ export default class ModalDiffModal extends Component {
|
|||||||
async updateResult(result) {
|
async updateResult(result) {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
|
|
||||||
|
if (result.done) {
|
||||||
|
this.finalResult = result.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.done) {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.args.model.showResultAsDiff) {
|
if (this.args.model.showResultAsDiff) {
|
||||||
this.diffStreamer.updateResult(result, "result");
|
this.diffStreamer.updateResult(result, "result");
|
||||||
} else {
|
} else {
|
||||||
@@ -84,9 +105,10 @@ export default class ModalDiffModal extends Component {
|
|||||||
data: {
|
data: {
|
||||||
location: "composer",
|
location: "composer",
|
||||||
mode: this.args.model.mode,
|
mode: this.args.model.mode,
|
||||||
text: this.args.model.selectedText,
|
text: this.selectedText,
|
||||||
custom_prompt: this.args.model.customPromptValue,
|
custom_prompt: this.args.model.customPromptValue,
|
||||||
force_default_locale: true,
|
force_default_locale: true,
|
||||||
|
client_id: this.messageBus.clientId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -100,16 +122,17 @@ export default class ModalDiffModal extends Component {
|
|||||||
|
|
||||||
if (this.suggestion) {
|
if (this.suggestion) {
|
||||||
this.args.model.toolbarEvent.replaceText(
|
this.args.model.toolbarEvent.replaceText(
|
||||||
this.args.model.selectedText,
|
this.selectedText,
|
||||||
this.suggestion
|
this.suggestion
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.args.model.showResultAsDiff && this.diffStreamer.suggestion) {
|
const finalResult =
|
||||||
this.args.model.toolbarEvent.replaceText(
|
this.finalResult?.length > 0
|
||||||
this.args.model.selectedText,
|
? this.finalResult
|
||||||
this.diffStreamer.suggestion
|
: this.diffStreamer.suggestion;
|
||||||
);
|
if (this.args.model.showResultAsDiff && finalResult) {
|
||||||
|
this.args.model.toolbarEvent.replaceText(this.selectedText, finalResult);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,10 +143,14 @@ export default class ModalDiffModal extends Component {
|
|||||||
@closeModal={{@closeModal}}
|
@closeModal={{@closeModal}}
|
||||||
>
|
>
|
||||||
<:body>
|
<:body>
|
||||||
<div {{didInsert this.subscribe}} {{willDestroy this.unsubscribe}}>
|
<div
|
||||||
|
{{didInsert this.subscribe}}
|
||||||
|
{{willDestroy this.unsubscribe}}
|
||||||
|
class="text-preview"
|
||||||
|
>
|
||||||
{{#if this.loading}}
|
{{#if this.loading}}
|
||||||
<div class="composer-ai-helper-modal__loading">
|
<div class="composer-ai-helper-modal__loading">
|
||||||
<CookText @rawText={{@model.selectedText}} />
|
{{~@model.selectedText~}}
|
||||||
</div>
|
</div>
|
||||||
{{else}}
|
{{else}}
|
||||||
<div
|
<div
|
||||||
@@ -132,10 +159,11 @@ export default class ModalDiffModal extends Component {
|
|||||||
"streamable-content"
|
"streamable-content"
|
||||||
(if this.isStreaming "streaming")
|
(if this.isStreaming "streaming")
|
||||||
(if @model.showResultAsDiff "inline-diff")
|
(if @model.showResultAsDiff "inline-diff")
|
||||||
|
(if this.diffStreamer.isThinking "thinking")
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{{#if @model.showResultAsDiff}}
|
{{~#if @model.showResultAsDiff~}}
|
||||||
{{htmlSafe this.diffStreamer.diff}}
|
<span class="diff-inner">{{htmlSafe this.diffResult}}</span>
|
||||||
{{else}}
|
{{else}}
|
||||||
{{#if this.smoothStreamer.isStreaming}}
|
{{#if this.smoothStreamer.isStreaming}}
|
||||||
<CookText
|
<CookText
|
||||||
|
|||||||
+16
-10
@@ -7,6 +7,10 @@ import AiSummaryModal from "../../components/modal/ai-summary-modal";
|
|||||||
export default class AiSummaryTrigger extends Component {
|
export default class AiSummaryTrigger extends Component {
|
||||||
@service modal;
|
@service modal;
|
||||||
|
|
||||||
|
get isAiConversation() {
|
||||||
|
return this.args.outletArgs.topic.is_bot_pm;
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
openAiSummaryModal() {
|
openAiSummaryModal() {
|
||||||
this.modal.show(AiSummaryModal, {
|
this.modal.show(AiSummaryModal, {
|
||||||
@@ -18,15 +22,17 @@ export default class AiSummaryTrigger extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
{{#if @outletArgs.topic.summarizable}}
|
{{#unless this.isAiConversation}}
|
||||||
<section class="topic-map__additional-contents toggle-summary">
|
{{#if @outletArgs.topic.summarizable}}
|
||||||
<DButton
|
<section class="topic-map__additional-contents toggle-summary">
|
||||||
@label="summary.buttons.generate"
|
<DButton
|
||||||
@icon="discourse-sparkles"
|
@label="summary.buttons.generate"
|
||||||
@action={{this.openAiSummaryModal}}
|
@icon="discourse-sparkles"
|
||||||
class="btn-default ai-summarization-button"
|
@action={{this.openAiSummaryModal}}
|
||||||
/>
|
class="btn-default ai-summarization-button"
|
||||||
</section>
|
/>
|
||||||
{{/if}}
|
</section>
|
||||||
|
{{/if}}
|
||||||
|
{{/unless}}
|
||||||
</template>
|
</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>
|
||||||
|
}
|
||||||
@@ -1,200 +0,0 @@
|
|||||||
import { tracked } from "@glimmer/tracking";
|
|
||||||
import Controller from "@ember/controller";
|
|
||||||
import { action } from "@ember/object";
|
|
||||||
import { getOwner } from "@ember/owner";
|
|
||||||
import { service } from "@ember/service";
|
|
||||||
import { TrackedArray } from "@ember-compat/tracked-built-ins";
|
|
||||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
|
||||||
import UppyUpload from "discourse/lib/uppy/uppy-upload";
|
|
||||||
import UppyMediaOptimization from "discourse/lib/uppy-media-optimization-plugin";
|
|
||||||
import { clipboardHelpers } from "discourse/lib/utilities";
|
|
||||||
|
|
||||||
export default class DiscourseAiBotConversations extends Controller {
|
|
||||||
@service aiBotConversationsHiddenSubmit;
|
|
||||||
@service currentUser;
|
|
||||||
@service mediaOptimizationWorker;
|
|
||||||
@service site;
|
|
||||||
@service siteSettings;
|
|
||||||
|
|
||||||
@tracked uploads = new TrackedArray();
|
|
||||||
// Don't track this directly - we'll get it from uppyUpload
|
|
||||||
|
|
||||||
textarea = null;
|
|
||||||
uppyUpload = null;
|
|
||||||
fileInputEl = null;
|
|
||||||
|
|
||||||
_handlePaste = (event) => {
|
|
||||||
if (document.activeElement !== this.textarea) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { canUpload, canPasteHtml, types } = clipboardHelpers(event, {
|
|
||||||
siteSettings: this.siteSettings,
|
|
||||||
canUpload: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!canUpload || canPasteHtml || types.includes("text/plain")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event && event.clipboardData && event.clipboardData.files) {
|
|
||||||
this.uppyUpload.addFiles([...event.clipboardData.files], {
|
|
||||||
pasted: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
init() {
|
|
||||||
super.init(...arguments);
|
|
||||||
|
|
||||||
this.uppyUpload = new UppyUpload(getOwner(this), {
|
|
||||||
id: "ai-bot-file-uploader",
|
|
||||||
type: "ai-bot-conversation",
|
|
||||||
useMultipartUploadsIfAvailable: true,
|
|
||||||
|
|
||||||
uppyReady: () => {
|
|
||||||
if (this.siteSettings.composer_media_optimization_image_enabled) {
|
|
||||||
this.uppyUpload.uppyWrapper.useUploadPlugin(UppyMediaOptimization, {
|
|
||||||
optimizeFn: (data, opts) =>
|
|
||||||
this.mediaOptimizationWorker.optimizeImage(data, opts),
|
|
||||||
runParallel: !this.site.isMobileDevice,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.uppyUpload.uppyWrapper.onPreProcessProgress((file) => {
|
|
||||||
const inProgressUpload = this.inProgressUploads?.find(
|
|
||||||
(upl) => upl.id === file.id
|
|
||||||
);
|
|
||||||
if (inProgressUpload && !inProgressUpload.processing) {
|
|
||||||
inProgressUpload.processing = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.uppyUpload.uppyWrapper.onPreProcessComplete((file) => {
|
|
||||||
const inProgressUpload = this.inProgressUploads?.find(
|
|
||||||
(upl) => upl.id === file.id
|
|
||||||
);
|
|
||||||
if (inProgressUpload) {
|
|
||||||
inProgressUpload.processing = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Setup paste listener for the textarea
|
|
||||||
this.textarea?.addEventListener("paste", this._handlePaste);
|
|
||||||
},
|
|
||||||
|
|
||||||
uploadDone: (upload) => {
|
|
||||||
this.uploads.push(upload);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Fix: Don't try to set inProgressUploads directly
|
|
||||||
onProgressUploadsChanged: () => {
|
|
||||||
// This is just for UI triggers - we're already tracking inProgressUploads
|
|
||||||
this.notifyPropertyChange("inProgressUploads");
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
willDestroy() {
|
|
||||||
super.willDestroy(...arguments);
|
|
||||||
this.textarea?.removeEventListener("paste", this._handlePaste);
|
|
||||||
this.uppyUpload?.teardown();
|
|
||||||
}
|
|
||||||
|
|
||||||
get loading() {
|
|
||||||
return this.aiBotConversationsHiddenSubmit?.loading;
|
|
||||||
}
|
|
||||||
|
|
||||||
get inProgressUploads() {
|
|
||||||
return this.uppyUpload?.inProgressUploads || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
get showUploadsContainer() {
|
|
||||||
return this.uploads?.length > 0 || this.inProgressUploads?.length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
setPersonaId(id) {
|
|
||||||
this.aiBotConversationsHiddenSubmit.personaId = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
setTargetRecipient(username) {
|
|
||||||
this.aiBotConversationsHiddenSubmit.targetUsername = username;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
updateInputValue(value) {
|
|
||||||
this._autoExpandTextarea();
|
|
||||||
this.aiBotConversationsHiddenSubmit.inputValue =
|
|
||||||
value.target?.value || value;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
handleKeyDown(event) {
|
|
||||||
if (event.key === "Enter" && !event.shiftKey) {
|
|
||||||
this.prepareAndSubmitToBot();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
setTextArea(element) {
|
|
||||||
this.textarea = element;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
registerFileInput(element) {
|
|
||||||
if (element) {
|
|
||||||
this.fileInputEl = element;
|
|
||||||
if (this.uppyUpload) {
|
|
||||||
this.uppyUpload.setup(element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
openFileUpload() {
|
|
||||||
if (this.fileInputEl) {
|
|
||||||
this.fileInputEl.click();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
removeUpload(upload) {
|
|
||||||
this.uploads = new TrackedArray(this.uploads.filter((u) => u !== upload));
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
cancelUpload(upload) {
|
|
||||||
this.uppyUpload.cancelSingleUpload({
|
|
||||||
fileId: upload.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
async prepareAndSubmitToBot() {
|
|
||||||
// Pass uploads to the service before submitting
|
|
||||||
this.aiBotConversationsHiddenSubmit.uploads = this.uploads;
|
|
||||||
try {
|
|
||||||
await this.aiBotConversationsHiddenSubmit.submitToBot();
|
|
||||||
this.uploads = new TrackedArray();
|
|
||||||
} catch (error) {
|
|
||||||
popupAjaxError(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_autoExpandTextarea() {
|
|
||||||
this.textarea.style.height = "auto";
|
|
||||||
this.textarea.style.height = this.textarea.scrollHeight + "px";
|
|
||||||
|
|
||||||
// Get the max-height value from CSS (30vh)
|
|
||||||
const maxHeight = parseInt(getComputedStyle(this.textarea).maxHeight, 10);
|
|
||||||
|
|
||||||
// Only enable scrolling if content exceeds max-height
|
|
||||||
if (this.textarea.scrollHeight > maxHeight) {
|
|
||||||
this.textarea.style.overflowY = "auto";
|
|
||||||
} else {
|
|
||||||
this.textarea.style.overflowY = "hidden";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,7 @@ import { later } from "@ember/runloop";
|
|||||||
import PostUpdater from "./updaters/post-updater";
|
import PostUpdater from "./updaters/post-updater";
|
||||||
|
|
||||||
const PROGRESS_INTERVAL = 40;
|
const PROGRESS_INTERVAL = 40;
|
||||||
const GIVE_UP_INTERVAL = 60000;
|
const GIVE_UP_INTERVAL = 600000; // 10 minutes which is our max thinking time for now
|
||||||
export const MIN_LETTERS_PER_INTERVAL = 6;
|
export const MIN_LETTERS_PER_INTERVAL = 6;
|
||||||
const MAX_FLUSH_TIME = 800;
|
const MAX_FLUSH_TIME = 800;
|
||||||
|
|
||||||
|
|||||||
@@ -12,13 +12,15 @@ export default class PostUpdater extends StreamUpdater {
|
|||||||
|
|
||||||
constructor(postStream, postId) {
|
constructor(postStream, postId) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.postStream = postStream;
|
this.postStream = postStream;
|
||||||
this.postId = postId;
|
this.postId = postId;
|
||||||
this.post = postStream.findLoadedPost(postId);
|
this.post = postStream.findLoadedPost(postId);
|
||||||
|
const topicId = postStream.topic.id;
|
||||||
|
|
||||||
if (this.post) {
|
if (this.post) {
|
||||||
this.postElement = document.querySelector(
|
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) {
|
async setCooked(value) {
|
||||||
|
if (!this.postElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.post.set("cooked", value);
|
this.post.set("cooked", value);
|
||||||
|
|
||||||
(await loadMorphlex()).morphInner(
|
(await loadMorphlex()).morphInner(
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@ import { ADMIN_PANEL, MAIN_PANEL } from "discourse/lib/sidebar/panels";
|
|||||||
export const AI_CONVERSATIONS_PANEL = "ai-conversations";
|
export const AI_CONVERSATIONS_PANEL = "ai-conversations";
|
||||||
|
|
||||||
export default class AiConversationsSidebarManager extends Service {
|
export default class AiConversationsSidebarManager extends Service {
|
||||||
|
@service appEvents;
|
||||||
@service sidebarState;
|
@service sidebarState;
|
||||||
|
|
||||||
@tracked newTopicForceSidebar = false;
|
@tracked newTopicForceSidebar = false;
|
||||||
@@ -26,6 +27,7 @@ export default class AiConversationsSidebarManager extends Service {
|
|||||||
|
|
||||||
this.sidebarState.isForcingSidebar = true;
|
this.sidebarState.isForcingSidebar = true;
|
||||||
document.body.classList.add("has-ai-conversations-sidebar");
|
document.body.classList.add("has-ai-conversations-sidebar");
|
||||||
|
this.appEvents.trigger("discourse-ai:force-conversations-sidebar");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,6 +42,7 @@ export default class AiConversationsSidebarManager extends Service {
|
|||||||
if (this.sidebarState.isForcingSidebar && !isAdminSidebarActive) {
|
if (this.sidebarState.isForcingSidebar && !isAdminSidebarActive) {
|
||||||
this.sidebarState.setPanel(MAIN_PANEL); // Return to main sidebar panel
|
this.sidebarState.setPanel(MAIN_PANEL); // Return to main sidebar panel
|
||||||
this.sidebarState.isForcingSidebar = false;
|
this.sidebarState.isForcingSidebar = false;
|
||||||
|
this.appEvents.trigger("discourse-ai:stop-forcing-conversations-sidebar");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,102 +1,4 @@
|
|||||||
import { fn, hash } from "@ember/helper";
|
|
||||||
import { on } from "@ember/modifier";
|
|
||||||
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
|
||||||
import RouteTemplate from "ember-route-template";
|
import RouteTemplate from "ember-route-template";
|
||||||
import DButton from "discourse/components/d-button";
|
import AiBotConversations from "discourse/plugins/discourse-ai/discourse/components/ai-bot-conversations";
|
||||||
import PluginOutlet from "discourse/components/plugin-outlet";
|
|
||||||
import { i18n } from "discourse-i18n";
|
|
||||||
import AiPersonaLlmSelector from "discourse/plugins/discourse-ai/discourse/components/ai-persona-llm-selector";
|
|
||||||
|
|
||||||
export default RouteTemplate(
|
export default RouteTemplate(<template><AiBotConversations /></template>);
|
||||||
<template>
|
|
||||||
<div class="ai-bot-conversations">
|
|
||||||
<AiPersonaLlmSelector
|
|
||||||
@showLabels={{true}}
|
|
||||||
@setPersonaId={{@controller.setPersonaId}}
|
|
||||||
@setTargetRecipient={{@controller.setTargetRecipient}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="ai-bot-conversations__content-wrapper">
|
|
||||||
<div class="ai-bot-conversations__title">
|
|
||||||
{{i18n "discourse_ai.ai_bot.conversations.header"}}
|
|
||||||
</div>
|
|
||||||
<PluginOutlet
|
|
||||||
@name="ai-bot-conversations-above-input"
|
|
||||||
@outletArgs={{hash
|
|
||||||
[email protected]
|
|
||||||
[email protected]
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="ai-bot-conversations__input-wrapper">
|
|
||||||
<DButton
|
|
||||||
@icon="upload"
|
|
||||||
@action={{@controller.openFileUpload}}
|
|
||||||
@title="discourse_ai.ai_bot.conversations.upload_files"
|
|
||||||
class="btn btn-transparent ai-bot-upload-btn"
|
|
||||||
/>
|
|
||||||
<textarea
|
|
||||||
{{didInsert @controller.setTextArea}}
|
|
||||||
{{on "input" @controller.updateInputValue}}
|
|
||||||
{{on "keydown" @controller.handleKeyDown}}
|
|
||||||
id="ai-bot-conversations-input"
|
|
||||||
autofocus="true"
|
|
||||||
placeholder={{i18n "discourse_ai.ai_bot.conversations.placeholder"}}
|
|
||||||
minlength="10"
|
|
||||||
disabled={{@controller.loading}}
|
|
||||||
rows="1"
|
|
||||||
/>
|
|
||||||
<DButton
|
|
||||||
@action={{@controller.prepareAndSubmitToBot}}
|
|
||||||
@icon="paper-plane"
|
|
||||||
@isLoading={{@controller.loading}}
|
|
||||||
@title="discourse_ai.ai_bot.conversations.header"
|
|
||||||
class="ai-bot-button btn-transparent ai-conversation-submit"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
id="ai-bot-file-uploader"
|
|
||||||
class="hidden-upload-field"
|
|
||||||
multiple="multiple"
|
|
||||||
{{didInsert @controller.registerFileInput}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="ai-disclaimer">
|
|
||||||
{{i18n "discourse_ai.ai_bot.conversations.disclaimer"}}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{{#if @controller.showUploadsContainer}}
|
|
||||||
<div class="ai-bot-conversations__uploads-container">
|
|
||||||
{{#each @controller.uploads as |upload|}}
|
|
||||||
<div class="ai-bot-upload">
|
|
||||||
<span class="ai-bot-upload__filename">
|
|
||||||
{{upload.original_filename}}
|
|
||||||
</span>
|
|
||||||
<DButton
|
|
||||||
@icon="xmark"
|
|
||||||
@action={{fn @controller.removeUpload upload}}
|
|
||||||
class="btn-transparent ai-bot-upload__remove"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{{/each}}
|
|
||||||
|
|
||||||
{{#each @controller.inProgressUploads as |upload|}}
|
|
||||||
<div class="ai-bot-upload ai-bot-upload--in-progress">
|
|
||||||
<span class="ai-bot-upload__filename">{{upload.fileName}}</span>
|
|
||||||
<span class="ai-bot-upload__progress">
|
|
||||||
{{upload.progress}}%
|
|
||||||
</span>
|
|
||||||
<DButton
|
|
||||||
@icon="xmark"
|
|
||||||
@action={{fn @controller.cancelUpload upload}}
|
|
||||||
class="btn-flat ai-bot-upload__cancel"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{{/each}}
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -18,12 +18,24 @@ function initializeAiArtifacts(api) {
|
|||||||
"data-ai-artifact-version"
|
"data-ai-artifact-version"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const dataAttributes = {};
|
||||||
|
for (const attr of artifactElement.attributes) {
|
||||||
|
if (
|
||||||
|
attr.name.startsWith("data-") &&
|
||||||
|
attr.name !== "data-ai-artifact-id" &&
|
||||||
|
attr.name !== "data-ai-artifact-version"
|
||||||
|
) {
|
||||||
|
dataAttributes[attr.name] = attr.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
helper.renderGlimmer(
|
helper.renderGlimmer(
|
||||||
artifactElement,
|
artifactElement,
|
||||||
<template>
|
<template>
|
||||||
<AiArtifact
|
<AiArtifact
|
||||||
@artifactId={{artifactId}}
|
@artifactId={{artifactId}}
|
||||||
@artifactVersion={{artifactVersion}}
|
@artifactVersion={{artifactVersion}}
|
||||||
|
@dataAttributes={{dataAttributes}}
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ export default {
|
|||||||
);
|
);
|
||||||
const appEvents = api.container.lookup("service:app-events");
|
const appEvents = api.container.lookup("service:app-events");
|
||||||
const messageBus = api.container.lookup("service:message-bus");
|
const messageBus = api.container.lookup("service:message-bus");
|
||||||
const navigationMenu = api.container.lookup("service:navigationMenu");
|
|
||||||
|
|
||||||
api.addSidebarPanel(
|
api.addSidebarPanel(
|
||||||
(BaseCustomSidebarPanel) =>
|
(BaseCustomSidebarPanel) =>
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ function initializeAiHelperTrigger(api) {
|
|||||||
icon: "discourse-sparkles",
|
icon: "discourse-sparkles",
|
||||||
title: "discourse_ai.ai_helper.context_menu.trigger",
|
title: "discourse_ai.ai_helper.context_menu.trigger",
|
||||||
preventFocus: true,
|
preventFocus: true,
|
||||||
|
hideShortcutInTitle: true,
|
||||||
shortcut: "ALT+P",
|
shortcut: "ALT+P",
|
||||||
shortcutAction: (toolbarEvent) => {
|
shortcutAction: (toolbarEvent) => {
|
||||||
if (toolbarEvent.getText().length === 0) {
|
if (toolbarEvent.getText().length === 0) {
|
||||||
|
|||||||
@@ -79,3 +79,52 @@ article.streaming .cooked {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes mark-blink {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
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,
|
.private-message-glyph-wrapper,
|
||||||
.topic-header-participants,
|
.topic-header-participants,
|
||||||
.topic-above-footer-buttons-outlet,
|
.topic-above-footer-buttons-outlet,
|
||||||
.topic-map,
|
|
||||||
#topic-footer-buttons .topic-footer-main-buttons details {
|
#topic-footer-buttons .topic-footer-main-buttons details {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -455,4 +454,82 @@ body.has-ai-conversations-sidebar {
|
|||||||
display: none;
|
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 {
|
.composer-ai-helper-modal {
|
||||||
.text-preview,
|
.text-preview,
|
||||||
.inline-diff {
|
.inline-diff {
|
||||||
|
font-family: var(--d-font-family--monospace);
|
||||||
|
font-variant-ligatures: none;
|
||||||
|
|
||||||
ins {
|
ins {
|
||||||
background-color: var(--success-low);
|
background-color: var(--success-low);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|||||||
@@ -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."
|
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."
|
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:
|
response_format:
|
||||||
|
title: "JSON-Antwortformat"
|
||||||
|
no_format: "Kein JSON-Format angegeben"
|
||||||
open_modal: "Bearbeiten"
|
open_modal: "Bearbeiten"
|
||||||
modal:
|
modal:
|
||||||
|
root_title: "Antwortstruktur"
|
||||||
key_title: "Schlüssel"
|
key_title: "Schlüssel"
|
||||||
|
examples:
|
||||||
|
title: Beispiele
|
||||||
|
new: Neues Beispiel
|
||||||
|
remove: Beispiel löschen
|
||||||
|
collapsable_title: "Beispiel #%{number}"
|
||||||
|
model: "Antwort des Modells"
|
||||||
list:
|
list:
|
||||||
enabled: "KI-Bot?"
|
enabled: "KI-Bot?"
|
||||||
ai_bot:
|
ai_bot:
|
||||||
@@ -514,6 +523,7 @@ de:
|
|||||||
disable_streaming: "Streaming-Vervollständigung deaktivieren (Streaming-Anfragen in Nicht-Streaming-Anfragen umwandeln)"
|
disable_streaming: "Streaming-Vervollständigung deaktivieren (Streaming-Anfragen in Nicht-Streaming-Anfragen umwandeln)"
|
||||||
reasoning_effort: "Argumentationsaufwand (gilt nur für Argumentationsmodelle)"
|
reasoning_effort: "Argumentationsaufwand (gilt nur für Argumentationsmodelle)"
|
||||||
enable_reasoning: "Argumentation aktivieren (gilt nur für Sonnet 3.7)"
|
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"
|
reasoning_tokens: "Anzahl der für die Argumentation verwendeten Token"
|
||||||
disable_temperature: "Temperatur deaktivieren (einige Denkmodelle unterstützen keine Temperatur)"
|
disable_temperature: "Temperatur deaktivieren (einige Denkmodelle unterstützen keine Temperatur)"
|
||||||
disable_top_p: "Top P deaktivieren (einige Denkmodelle unterstützen Top P nicht)"
|
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."
|
disclaimer: "Generative KI kann Fehler machen. Überprüfe wichtige Informationen."
|
||||||
placeholder: "Stell eine Frage ..."
|
placeholder: "Stell eine Frage ..."
|
||||||
new: "Neue 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"
|
messages_sidebar_title: "Unterhaltungen"
|
||||||
today: "Heute"
|
today: "Heute"
|
||||||
last_7_days: "Letzte 7 Tage"
|
last_7_days: "Letzte 7 Tage"
|
||||||
|
|||||||
@@ -712,6 +712,10 @@ en:
|
|||||||
name: "Share AI conversation"
|
name: "Share AI conversation"
|
||||||
title: "Share this AI conversation publicly"
|
title: "Share this AI conversation publicly"
|
||||||
|
|
||||||
|
invite_ai_conversation:
|
||||||
|
button: "Invite"
|
||||||
|
title: "Invite to AI conversation"
|
||||||
|
|
||||||
ai_label: "AI"
|
ai_label: "AI"
|
||||||
ai_title: "Conversation with AI"
|
ai_title: "Conversation with AI"
|
||||||
|
|
||||||
|
|||||||
@@ -309,6 +309,13 @@ he:
|
|||||||
modal:
|
modal:
|
||||||
root_title: "מבנה תגובה"
|
root_title: "מבנה תגובה"
|
||||||
key_title: "מפתח"
|
key_title: "מפתח"
|
||||||
|
examples:
|
||||||
|
title: דוגמאות
|
||||||
|
new: דוגמה חדשה
|
||||||
|
remove: מחיקת דוגמה
|
||||||
|
collapsable_title: "דוגמה מס׳ %{number}"
|
||||||
|
user: "הודעת משתמש"
|
||||||
|
model: "תגובת המודל"
|
||||||
list:
|
list:
|
||||||
enabled: "בוט בינה מלאכותית?"
|
enabled: "בוט בינה מלאכותית?"
|
||||||
ai_bot:
|
ai_bot:
|
||||||
@@ -714,6 +721,7 @@ he:
|
|||||||
discobot_discoveries:
|
discobot_discoveries:
|
||||||
regular_results: "נושאים"
|
regular_results: "נושאים"
|
||||||
tell_me_more: "אשמח לדעת עוד…"
|
tell_me_more: "אשמח לדעת עוד…"
|
||||||
|
continue_convo: "המשך בשיחה…"
|
||||||
collapse: "צמצום"
|
collapse: "צמצום"
|
||||||
tooltip:
|
tooltip:
|
||||||
header: "חיפוש מופעל בינה מלאכותית"
|
header: "חיפוש מופעל בינה מלאכותית"
|
||||||
|
|||||||
@@ -261,9 +261,6 @@ ar:
|
|||||||
settings_explorer:
|
settings_explorer:
|
||||||
name: مستكشف الإعدادات
|
name: مستكشف الإعدادات
|
||||||
description: "روبوت ذكاء اصطناعي متخصص في المساعدة في استكشاف إعدادات موقع Discourse"
|
description: "روبوت ذكاء اصطناعي متخصص في المساعدة في استكشاف إعدادات موقع Discourse"
|
||||||
researcher:
|
|
||||||
name: الباحث
|
|
||||||
description: "روبوت ذكاء اصطناعي مزوَّد بإمكانية الوصول إلى Google، ويمكنه البحث عن المعلومات نيابةً عنك"
|
|
||||||
creative:
|
creative:
|
||||||
name: المبدع
|
name: المبدع
|
||||||
description: "روبوت ذكاء اصطناعي من دون تكاملات خارجية متخصصة في المهام الإبداعية"
|
description: "روبوت ذكاء اصطناعي من دون تكاملات خارجية متخصصة في المهام الإبداعية"
|
||||||
@@ -280,6 +277,9 @@ ar:
|
|||||||
summarizing: "جارٍ تلخيص الموضوع"
|
summarizing: "جارٍ تلخيص الموضوع"
|
||||||
searching: "جارٍ البحث عن: '%{query}'"
|
searching: "جارٍ البحث عن: '%{query}'"
|
||||||
tool_options:
|
tool_options:
|
||||||
|
researcher:
|
||||||
|
max_results:
|
||||||
|
name: "الحد الأقصى لعدد النتائج"
|
||||||
google:
|
google:
|
||||||
base_query:
|
base_query:
|
||||||
name: "استعلام البحث الأساسي"
|
name: "استعلام البحث الأساسي"
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ de:
|
|||||||
ai_discord_search_mode: "Wähle den Suchmodus für die Discord-Suche aus"
|
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_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_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:
|
reviewables:
|
||||||
reasons:
|
reasons:
|
||||||
flagged_by_toxicity: Das KI-Plug-in meldete dies nach der Klassifizierung als toxisch.
|
flagged_by_toxicity: Das KI-Plug-in meldete dies nach der Klassifizierung als toxisch.
|
||||||
@@ -278,6 +279,11 @@ de:
|
|||||||
artist:
|
artist:
|
||||||
name: Künstler
|
name: Künstler
|
||||||
description: "KI-Bot, spezialisiert auf die Erstellung von Bildern"
|
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:
|
sql_helper:
|
||||||
name: SQL-Helfer
|
name: SQL-Helfer
|
||||||
description: "KI-Bot, der darauf spezialisiert ist, bei der Erstellung von SQL-Abfragen auf dieser Discourse-Instanz zu helfen"
|
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
|
name: Einstellungs-Explorer
|
||||||
description: "KI-Bot, der darauf spezialisiert ist, die Einstellungen der Discourse-Website zu erkunden"
|
description: "KI-Bot, der darauf spezialisiert ist, die Einstellungen der Discourse-Website zu erkunden"
|
||||||
researcher:
|
researcher:
|
||||||
name: Forscher
|
name: Web-Rechercheur
|
||||||
description: "KI-Bot mit Google-Zugang, der Informationen für dich recherchieren kann"
|
description: "KI-Bot mit Google-Zugriff, der Webseiten sowohl suchen als auch lesen kann"
|
||||||
creative:
|
creative:
|
||||||
name: Kreativ
|
name: Kreativ
|
||||||
description: "KI-Bot ohne externe Integrationen, spezialisiert auf kreative Aufgaben"
|
description: "KI-Bot ohne externe Integrationen, spezialisiert auf kreative Aufgaben"
|
||||||
@@ -309,6 +315,16 @@ de:
|
|||||||
summarizing: "Thema zusammenfassen"
|
summarizing: "Thema zusammenfassen"
|
||||||
searching: "Suche nach: „%{query}“"
|
searching: "Suche nach: „%{query}“"
|
||||||
tool_options:
|
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:
|
create_artifact:
|
||||||
creator_llm:
|
creator_llm:
|
||||||
name: "LLM"
|
name: "LLM"
|
||||||
@@ -365,6 +381,7 @@ de:
|
|||||||
dall_e: "Bild generieren"
|
dall_e: "Bild generieren"
|
||||||
search_meta_discourse: "Meta-Discourse durchsuchen"
|
search_meta_discourse: "Meta-Discourse durchsuchen"
|
||||||
javascript_evaluator: "JavaScript auswerten"
|
javascript_evaluator: "JavaScript auswerten"
|
||||||
|
researcher: "Rechercheur"
|
||||||
tool_help:
|
tool_help:
|
||||||
read_artifact: "Lesen eines Webartefakts mit dem KI-Bot"
|
read_artifact: "Lesen eines Webartefakts mit dem KI-Bot"
|
||||||
update_artifact: "Aktualisiere ein Web-Artefakt 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"
|
dall_e: "Bild mit DALL-E 3 generieren"
|
||||||
search_meta_discourse: "Meta-Discourse durchsuchen"
|
search_meta_discourse: "Meta-Discourse durchsuchen"
|
||||||
javascript_evaluator: "JavaScript auswerten"
|
javascript_evaluator: "JavaScript auswerten"
|
||||||
|
researcher: "Foreninformationen mit dem KI-Bot recherchieren"
|
||||||
tool_description:
|
tool_description:
|
||||||
read_artifact: "Lesen eines Webartefakts mit dem KI-Bot"
|
read_artifact: "Lesen eines Webartefakts mit dem KI-Bot"
|
||||||
update_artifact: "Ein Web-Artefakt wurde mit dem KI-Bot aktualisiert"
|
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"
|
other: "%{count} <a href='%{url}'>Ergebnisse</a> für „%{query}“ gefunden"
|
||||||
setting_context: "Kontext wird gelesen für: %{setting_name}"
|
setting_context: "Kontext wird gelesen für: %{setting_name}"
|
||||||
schema: "%{tables}"
|
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:
|
search_settings:
|
||||||
one: "%{count} Ergebnis für „%{query}“ gefunden"
|
one: "%{count} Ergebnis für „%{query}“ gefunden"
|
||||||
other: "%{count} Ergebnisse 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."
|
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."
|
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."
|
cannot_edit_builtin: "Du kannst ein integriertes Modell nicht bearbeiten."
|
||||||
|
personas:
|
||||||
|
malformed_examples: "Die angegebenen Beispiele haben das falsche Format."
|
||||||
embeddings:
|
embeddings:
|
||||||
delete_failed: "Dieses Modell wird derzeit verwendet. Aktualisiere zuerst `ai embeddings selected model`."
|
delete_failed: "Dieses Modell wird derzeit verwendet. Aktualisiere zuerst `ai embeddings selected model`."
|
||||||
cannot_edit_builtin: "Du kannst ein integriertes Modell nicht bearbeiten."
|
cannot_edit_builtin: "Du kannst ein integriertes Modell nicht bearbeiten."
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ en:
|
|||||||
ai:
|
ai:
|
||||||
flag_types:
|
flag_types:
|
||||||
review: "Add post to review queue"
|
review: "Add post to review queue"
|
||||||
|
review_hide: "Add post to review queue and hide post"
|
||||||
spam: "Flag as spam and hide post"
|
spam: "Flag as spam and hide post"
|
||||||
spam_silence: "Flag as spam, hide post and silence user"
|
spam_silence: "Flag as spam, hide post and silence user"
|
||||||
scriptables:
|
scriptables:
|
||||||
@@ -296,6 +297,9 @@ en:
|
|||||||
designer:
|
designer:
|
||||||
name: Designer
|
name: Designer
|
||||||
description: "AI Bot specialized in generating and editing images"
|
description: "AI Bot specialized in generating and editing images"
|
||||||
|
forum_researcher:
|
||||||
|
name: Forum Researcher
|
||||||
|
description: "AI Bot specialized in deep research for the forum"
|
||||||
sql_helper:
|
sql_helper:
|
||||||
name: SQL Helper
|
name: SQL Helper
|
||||||
description: "AI Bot specialized in helping craft SQL queries on this Discourse instance"
|
description: "AI Bot specialized in helping craft SQL queries on this Discourse instance"
|
||||||
@@ -303,8 +307,8 @@ en:
|
|||||||
name: Settings Explorer
|
name: Settings Explorer
|
||||||
description: "AI Bot specialized in helping explore Discourse site settings"
|
description: "AI Bot specialized in helping explore Discourse site settings"
|
||||||
researcher:
|
researcher:
|
||||||
name: Researcher
|
name: Web Researcher
|
||||||
description: "AI Bot with Google access that can research information for you"
|
description: "AI Bot with Google access that can both search and read web pages"
|
||||||
creative:
|
creative:
|
||||||
name: Creative
|
name: Creative
|
||||||
description: "AI Bot with no external integrations specialized in creative tasks"
|
description: "AI Bot with no external integrations specialized in creative tasks"
|
||||||
@@ -327,6 +331,16 @@ en:
|
|||||||
summarizing: "Summarizing topic"
|
summarizing: "Summarizing topic"
|
||||||
searching: "Searching for: '%{query}'"
|
searching: "Searching for: '%{query}'"
|
||||||
tool_options:
|
tool_options:
|
||||||
|
researcher:
|
||||||
|
max_results:
|
||||||
|
name: "Maximum number of results"
|
||||||
|
description: "Maximum number of results to include in a filter"
|
||||||
|
include_private:
|
||||||
|
name: "Include private"
|
||||||
|
description: "Include private topics in the filters"
|
||||||
|
max_tokens_per_post:
|
||||||
|
name: "Maximum tokens per post"
|
||||||
|
description: "Maximum number of tokens to use for each post in the filter"
|
||||||
create_artifact:
|
create_artifact:
|
||||||
creator_llm:
|
creator_llm:
|
||||||
name: "LLM"
|
name: "LLM"
|
||||||
@@ -385,6 +399,8 @@ en:
|
|||||||
javascript_evaluator: "Evaluate JavaScript"
|
javascript_evaluator: "Evaluate JavaScript"
|
||||||
create_image: "Creating image"
|
create_image: "Creating image"
|
||||||
edit_image: "Editing image"
|
edit_image: "Editing image"
|
||||||
|
researcher: "Researching"
|
||||||
|
researcher_dry_run: "Preparing research"
|
||||||
tool_help:
|
tool_help:
|
||||||
read_artifact: "Read a web artifact using the AI Bot"
|
read_artifact: "Read a web artifact using the AI Bot"
|
||||||
update_artifact: "Update a web artifact using the AI Bot"
|
update_artifact: "Update a web artifact using the AI Bot"
|
||||||
@@ -411,6 +427,7 @@ en:
|
|||||||
dall_e: "Generate image using DALL-E 3"
|
dall_e: "Generate image using DALL-E 3"
|
||||||
search_meta_discourse: "Search Meta Discourse"
|
search_meta_discourse: "Search Meta Discourse"
|
||||||
javascript_evaluator: "Evaluate JavaScript"
|
javascript_evaluator: "Evaluate JavaScript"
|
||||||
|
researcher: "Research forum information using the AI Bot"
|
||||||
tool_description:
|
tool_description:
|
||||||
read_artifact: "Read a web artifact using the AI Bot"
|
read_artifact: "Read a web artifact using the AI Bot"
|
||||||
update_artifact: "Updated a web artifact using the AI Bot"
|
update_artifact: "Updated a web artifact using the AI Bot"
|
||||||
@@ -445,6 +462,12 @@ en:
|
|||||||
other: "Found %{count} <a href='%{url}'>results</a> for '%{query}'"
|
other: "Found %{count} <a href='%{url}'>results</a> for '%{query}'"
|
||||||
setting_context: "Reading context for: %{setting_name}"
|
setting_context: "Reading context for: %{setting_name}"
|
||||||
schema: "%{tables}"
|
schema: "%{tables}"
|
||||||
|
researcher_dry_run:
|
||||||
|
one: "Proposed goals: %{goals}\n\nFound %{count} post matching '%{filter}'"
|
||||||
|
other: "Proposed goals: %{goals}\n\nFound %{count} posts matching '%{filter}'"
|
||||||
|
researcher:
|
||||||
|
one: "Researching: %{goals}\n\nFound %{count} post matching '%{filter}'"
|
||||||
|
other: "Researching: %{goals}\n\nFound %{count} posts matching '%{filter}'"
|
||||||
search_settings:
|
search_settings:
|
||||||
one: "Found %{count} result for '%{query}'"
|
one: "Found %{count} result for '%{query}'"
|
||||||
other: "Found %{count} results for '%{query}'"
|
other: "Found %{count} results for '%{query}'"
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user