Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4194d3fb2 | ||
|
|
eab6dd3f8e | ||
|
|
baaa3d199a | ||
|
|
6a33e5154d | ||
|
|
37dbd48513 | ||
|
|
3e87e92631 | ||
|
|
cd14b0c0be | ||
|
|
cea8fd423e | ||
|
|
d7a2af5505 | ||
|
|
6dbe19a772 | ||
|
|
62d746662a | ||
|
|
9dccc1eb93 | ||
|
|
df925f8304 |
@@ -1,3 +1,4 @@
|
||||
< 3.5.0.beta7-dev: cd14b0c0bee0cf63c59b64b6f7213e31a37f11a7
|
||||
< 3.5.0.beta6-dev: 3e74eea1e5e3143888d67a8d8a11206df214dc24
|
||||
< 3.5.0.beta3-dev: 09a68414804a1447f52e5d60691ba59742cda9ec
|
||||
< 3.5.0.beta2-dev: de8624416a15b3d8e7ad350b083cc1420451ccec
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
module Jobs
|
||||
class DetectTranslatePost < ::Jobs::Base
|
||||
cluster_concurrency 1
|
||||
sidekiq_options retry: false
|
||||
|
||||
def execute(args)
|
||||
@@ -35,7 +36,7 @@ module Jobs
|
||||
end
|
||||
end
|
||||
|
||||
locales = SiteSetting.experimental_content_localization_supported_locales.split("|")
|
||||
locales = SiteSetting.content_localization_supported_locales.split("|")
|
||||
return if locales.blank?
|
||||
|
||||
locales.each do |locale|
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
module Jobs
|
||||
class DetectTranslateTopic < ::Jobs::Base
|
||||
cluster_concurrency 1
|
||||
sidekiq_options retry: false
|
||||
|
||||
def execute(args)
|
||||
@@ -33,7 +34,7 @@ module Jobs
|
||||
end
|
||||
end
|
||||
|
||||
locales = SiteSetting.experimental_content_localization_supported_locales.split("|")
|
||||
locales = SiteSetting.content_localization_supported_locales.split("|")
|
||||
return if locales.blank?
|
||||
|
||||
locales.each do |locale|
|
||||
|
||||
@@ -11,7 +11,7 @@ module Jobs
|
||||
return if !SiteSetting.discourse_ai_enabled
|
||||
return if !SiteSetting.ai_translation_enabled
|
||||
|
||||
locales = SiteSetting.experimental_content_localization_supported_locales.split("|")
|
||||
locales = SiteSetting.content_localization_supported_locales.split("|")
|
||||
return if locales.blank?
|
||||
|
||||
cat_id = args[:from_category_id] || Category.order(:id).first&.id
|
||||
|
||||
@@ -11,7 +11,7 @@ module Jobs
|
||||
return if !SiteSetting.discourse_ai_enabled
|
||||
return if !SiteSetting.ai_translation_enabled
|
||||
|
||||
locales = SiteSetting.experimental_content_localization_supported_locales.split("|")
|
||||
locales = SiteSetting.content_localization_supported_locales.split("|")
|
||||
return if locales.blank?
|
||||
|
||||
limit = args[:limit] || BATCH_SIZE
|
||||
|
||||
@@ -11,7 +11,7 @@ module Jobs
|
||||
return if !SiteSetting.discourse_ai_enabled
|
||||
return if !SiteSetting.ai_translation_enabled
|
||||
|
||||
locales = SiteSetting.experimental_content_localization_supported_locales.split("|")
|
||||
locales = SiteSetting.content_localization_supported_locales.split("|")
|
||||
return if locales.blank?
|
||||
|
||||
limit = args[:limit] || BATCH_SIZE
|
||||
|
||||
@@ -8,7 +8,7 @@ module Jobs
|
||||
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.content_localization_supported_locales.blank?
|
||||
|
||||
Jobs.enqueue(:localize_categories)
|
||||
end
|
||||
|
||||
@@ -9,7 +9,7 @@ module Jobs
|
||||
return if !SiteSetting.discourse_ai_enabled
|
||||
return if !SiteSetting.ai_translation_enabled
|
||||
|
||||
return if SiteSetting.experimental_content_localization_supported_locales.blank?
|
||||
return if SiteSetting.content_localization_supported_locales.blank?
|
||||
return if SiteSetting.ai_translation_backfill_rate == 0
|
||||
|
||||
Jobs.enqueue(:localize_posts, limit: SiteSetting.ai_translation_backfill_rate)
|
||||
|
||||
@@ -9,7 +9,7 @@ module Jobs
|
||||
return if !SiteSetting.discourse_ai_enabled
|
||||
return if !SiteSetting.ai_translation_enabled
|
||||
|
||||
return if SiteSetting.experimental_content_localization_supported_locales.blank?
|
||||
return if SiteSetting.content_localization_supported_locales.blank?
|
||||
return if SiteSetting.ai_translation_backfill_rate == 0
|
||||
|
||||
Jobs.enqueue(:localize_topics, limit: SiteSetting.ai_translation_backfill_rate)
|
||||
|
||||
@@ -24,6 +24,7 @@ class EmbeddingDefinition < ActiveRecord::Base
|
||||
DiscourseAi::Tokenizer::MultilingualE5LargeTokenizer,
|
||||
DiscourseAi::Tokenizer::OpenAiTokenizer,
|
||||
DiscourseAi::Tokenizer::MixtralTokenizer,
|
||||
DiscourseAi::Tokenizer::QwenTokenizer,
|
||||
].map(&:name)
|
||||
end
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ export default class AiLlmsListEditor extends Component {
|
||||
key = `${llm.provider}-${llm.name}`;
|
||||
} else {
|
||||
// case of preset
|
||||
key = llm.id.replace(/\./g, "-");
|
||||
key = llm.id.replace(/[.:\/]/g, "-");
|
||||
}
|
||||
|
||||
key = `discourse_ai.llms.model_description.${key}`;
|
||||
|
||||
@@ -14,7 +14,6 @@ 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 { withPluginApi } from "discourse/lib/plugin-api";
|
||||
import { sanitize } from "discourse/lib/text";
|
||||
import { clipboardCopy } from "discourse/lib/utilities";
|
||||
import { i18n } from "discourse-i18n";
|
||||
@@ -44,6 +43,9 @@ export default class AiPostHelperMenu extends Component {
|
||||
@tracked lastSelectedOption = null;
|
||||
@tracked isSavingFootnote = false;
|
||||
@tracked supportsAddFootnote = this.args.data.supportsFastEdit;
|
||||
@tracked
|
||||
channel =
|
||||
`/discourse-ai/ai-helper/stream_suggestion/${this.args.data.quoteState.postId}`;
|
||||
|
||||
@tracked
|
||||
smoothStreamer = new SmoothStreamer(
|
||||
@@ -78,23 +80,6 @@ export default class AiPostHelperMenu extends Component {
|
||||
|
||||
@tracked _activeAiRequest = null;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
|
||||
withPluginApi((api) => {
|
||||
api.registerValueTransformer(
|
||||
"post-text-selection-prevent-close",
|
||||
({ value }) => {
|
||||
if (this.menuState === this.MENU_STATES.result) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
get footnoteDisabled() {
|
||||
return this.streaming || !this.supportsAddFootnote;
|
||||
}
|
||||
@@ -167,16 +152,17 @@ export default class AiPostHelperMenu extends Component {
|
||||
|
||||
@bind
|
||||
subscribe() {
|
||||
const channel = `/discourse-ai/ai-helper/stream_suggestion/${this.args.data.quoteState.postId}`;
|
||||
this.messageBus.subscribe(channel, this._updateResult);
|
||||
this.messageBus.subscribe(
|
||||
this.channel,
|
||||
(data) => this._updateResult(data),
|
||||
this.args.data.post
|
||||
.discourse_ai_helper_stream_suggestion_last_message_bus_id
|
||||
);
|
||||
}
|
||||
|
||||
@bind
|
||||
unsubscribe() {
|
||||
this.messageBus.unsubscribe(
|
||||
"/discourse-ai/ai-helper/stream_suggestion/*",
|
||||
this._updateResult
|
||||
);
|
||||
this.messageBus.unsubscribe(this.channel, this._updateResult);
|
||||
}
|
||||
|
||||
@bind
|
||||
@@ -239,7 +225,7 @@ export default class AiPostHelperMenu extends Component {
|
||||
data: {
|
||||
location: "post",
|
||||
mode: option.name,
|
||||
text: this.args.data.selectedText,
|
||||
text: this.args.data.quoteState.buffer,
|
||||
post_id: this.args.data.quoteState.postId,
|
||||
custom_prompt: this.customPromptValue,
|
||||
client_id: this.messageBus.clientId,
|
||||
@@ -292,12 +278,10 @@ export default class AiPostHelperMenu extends Component {
|
||||
|
||||
@action
|
||||
closeMenu() {
|
||||
if (this.site.mobileView) {
|
||||
return this.args.close();
|
||||
}
|
||||
|
||||
const menu = this.menu.getByIdentifier("post-text-selection-toolbar");
|
||||
return menu?.close();
|
||||
// reset state and close
|
||||
this.suggestion = "";
|
||||
this.customPromptValue = "";
|
||||
return this.args.close();
|
||||
}
|
||||
|
||||
@action
|
||||
@@ -317,9 +301,9 @@ export default class AiPostHelperMenu extends Component {
|
||||
const credits = i18n(
|
||||
"discourse_ai.ai_helper.post_options_menu.footnote_credits"
|
||||
);
|
||||
const withFootnote = `${this.args.data.selectedText} ^[${sanitizedSuggestion} (${credits})]`;
|
||||
const withFootnote = `${this.args.data.quoteState.buffer} ^[${sanitizedSuggestion} (${credits})]`;
|
||||
const newRaw = result.raw.replace(
|
||||
this.args.data.selectedText,
|
||||
this.args.data.quoteState.buffer,
|
||||
withFootnote
|
||||
);
|
||||
|
||||
@@ -338,7 +322,7 @@ export default class AiPostHelperMenu extends Component {
|
||||
(and this.site.mobileView (eq this.menuState this.MENU_STATES.options))
|
||||
}}
|
||||
<div class="ai-post-helper-menu__selected-text">
|
||||
{{@data.selectedText}}
|
||||
{{@data.quoteState.buffer}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
|
||||
@@ -85,14 +85,18 @@ export default class ModalDiffModal extends Component {
|
||||
|
||||
@bind
|
||||
subscribe() {
|
||||
this.messageBus.subscribe(CHANNEL, this.updateResult);
|
||||
this.messageBus.subscribe(
|
||||
CHANNEL,
|
||||
this.updateResult,
|
||||
this.currentUser
|
||||
?.discourse_ai_helper_stream_composer_suggestion_last_message_bus_id
|
||||
);
|
||||
}
|
||||
|
||||
@bind
|
||||
cleanup() {
|
||||
// stop all callbacks so it does not end up streaming pointlessly
|
||||
this.smoothStreamer.resetStreaming();
|
||||
this.diffStreamer.reset();
|
||||
this.#resetState();
|
||||
this.messageBus.unsubscribe(CHANNEL, this.updateResult);
|
||||
}
|
||||
|
||||
@@ -114,8 +118,7 @@ export default class ModalDiffModal extends Component {
|
||||
|
||||
@action
|
||||
async suggestChanges() {
|
||||
this.smoothStreamer.resetStreaming();
|
||||
this.diffStreamer.reset();
|
||||
this.#resetState();
|
||||
|
||||
try {
|
||||
this.loading = true;
|
||||
@@ -159,11 +162,25 @@ export default class ModalDiffModal extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
cleanupAndClose() {
|
||||
this.#resetState();
|
||||
this.loading = false;
|
||||
this.args.closeModal();
|
||||
}
|
||||
|
||||
#resetState() {
|
||||
this.suggestion = "";
|
||||
this.finalResult = "";
|
||||
this.smoothStreamer.resetStreaming();
|
||||
this.diffStreamer.reset();
|
||||
}
|
||||
|
||||
<template>
|
||||
<DModal
|
||||
class="composer-ai-helper-modal"
|
||||
@title={{i18n "discourse_ai.ai_helper.context_menu.changes"}}
|
||||
@closeModal={{@closeModal}}
|
||||
@closeModal={{this.cleanupAndClose}}
|
||||
>
|
||||
<:body>
|
||||
<div
|
||||
@@ -218,7 +235,7 @@ export default class ModalDiffModal extends Component {
|
||||
</DButton>
|
||||
<DButton
|
||||
class="btn-flat discard"
|
||||
@action={{@closeModal}}
|
||||
@action={{this.cleanupAndClose}}
|
||||
@label="discourse_ai.ai_helper.context_menu.discard"
|
||||
/>
|
||||
<DButton
|
||||
|
||||
+45
-88
@@ -3,8 +3,7 @@ import { tracked } from "@glimmer/tracking";
|
||||
import { action } from "@ember/object";
|
||||
import { service } from "@ember/service";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import virtualElementFromTextRange from "discourse/lib/virtual-element-from-text-range";
|
||||
import eq from "truth-helpers/helpers/eq";
|
||||
import { selectedRange } from "discourse/lib/utilities";
|
||||
import AiPostHelperMenu from "../../components/ai-post-helper-menu";
|
||||
import { showPostAIHelper } from "../../lib/show-ai-helper";
|
||||
|
||||
@@ -13,27 +12,27 @@ export default class AiPostHelperTrigger extends Component {
|
||||
return showPostAIHelper(outletArgs, helper);
|
||||
}
|
||||
|
||||
@service site;
|
||||
@service menu;
|
||||
|
||||
@tracked menuState = this.MENU_STATES.triggers;
|
||||
@tracked showMainButtons = true;
|
||||
@tracked showAiButtons = true;
|
||||
@tracked postHighlighted = false;
|
||||
@tracked currentMenu = this.menu.getByIdentifier(
|
||||
"post-text-selection-toolbar"
|
||||
);
|
||||
|
||||
MENU_STATES = {
|
||||
triggers: "TRIGGERS",
|
||||
options: "OPTIONS",
|
||||
// capture the state at the moment the toolbar is rendered
|
||||
// so we ensure change of state (selection change for example)
|
||||
// is not impacting the menu data
|
||||
menuData = {
|
||||
...this.args.outletArgs.data,
|
||||
quoteState: {
|
||||
buffer: this.args.outletArgs.data.quoteState.buffer,
|
||||
opts: this.args.outletArgs.data.quoteState.opts,
|
||||
postId: this.args.outletArgs.data.quoteState.postId,
|
||||
},
|
||||
post: this.args.outletArgs.post,
|
||||
selectedRange: selectedRange(),
|
||||
};
|
||||
|
||||
willDestroy() {
|
||||
super.willDestroy(...arguments);
|
||||
this.removeHighlightedText();
|
||||
}
|
||||
|
||||
highlightSelectedText() {
|
||||
const postId = this.args.outletArgs.data.quoteState.postId;
|
||||
const postElement = document.querySelector(
|
||||
@@ -44,14 +43,7 @@ export default class AiPostHelperTrigger extends Component {
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectedText = this.args.outletArgs.data.quoteState.buffer;
|
||||
|
||||
const selection = window.getSelection();
|
||||
if (!selection.rangeCount) {
|
||||
return;
|
||||
}
|
||||
|
||||
const range = selection.getRangeAt(0);
|
||||
const range = this.menuData.selectedRange;
|
||||
|
||||
// Split start/end text nodes at their range boundary
|
||||
if (
|
||||
@@ -97,11 +89,10 @@ export default class AiPostHelperTrigger extends Component {
|
||||
// Replace textNode with highlighted clone
|
||||
const clone = textNode.cloneNode(true);
|
||||
highlight.appendChild(clone);
|
||||
|
||||
textNode.parentNode.replaceChild(highlight, textNode);
|
||||
}
|
||||
|
||||
selection.removeAllRanges();
|
||||
window.getSelection().removeAllRanges();
|
||||
this.postHighlighted = true;
|
||||
}
|
||||
|
||||
@@ -110,16 +101,7 @@ export default class AiPostHelperTrigger extends Component {
|
||||
return;
|
||||
}
|
||||
|
||||
const postId = this.args.outletArgs.data.quoteState.postId;
|
||||
const postElement = document.querySelector(
|
||||
`article[data-post-id='${postId}'] .cooked`
|
||||
);
|
||||
|
||||
if (!postElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const highlightedSpans = postElement.querySelectorAll(
|
||||
const highlightedSpans = document.querySelectorAll(
|
||||
"span.ai-helper-highlighted-selection"
|
||||
);
|
||||
|
||||
@@ -133,65 +115,40 @@ export default class AiPostHelperTrigger extends Component {
|
||||
|
||||
@action
|
||||
async showAiPostHelperMenu() {
|
||||
this.highlightSelectedText();
|
||||
if (this.site.mobileView) {
|
||||
this.currentMenu.close();
|
||||
await this.currentMenu.close();
|
||||
|
||||
await this.menu.show(virtualElementFromTextRange(), {
|
||||
identifier: "ai-post-helper-menu",
|
||||
component: AiPostHelperMenu,
|
||||
inline: true,
|
||||
interactive: true,
|
||||
placement: this.shouldRenderUnder ? "bottom-start" : "top-start",
|
||||
fallbackPlacements: this.shouldRenderUnder
|
||||
? ["bottom-end", "top-start"]
|
||||
: ["bottom-start"],
|
||||
trapTab: false,
|
||||
closeOnScroll: false,
|
||||
modalForMobile: true,
|
||||
data: this.menuData,
|
||||
});
|
||||
}
|
||||
|
||||
this.showMainButtons = false;
|
||||
this.menuState = this.MENU_STATES.options;
|
||||
}
|
||||
|
||||
get menuData() {
|
||||
// Streamline of data model to be passed to the component when
|
||||
// instantiated as a DMenu or a simple component in the template
|
||||
return {
|
||||
...this.args.outletArgs.data,
|
||||
quoteState: {
|
||||
buffer: this.args.outletArgs.data.quoteState.buffer,
|
||||
opts: this.args.outletArgs.data.quoteState.opts,
|
||||
postId: this.args.outletArgs.data.quoteState.postId,
|
||||
await this.menu.show(this.currentMenu.trigger, {
|
||||
identifier: "ai-post-helper-menu",
|
||||
component: AiPostHelperMenu,
|
||||
interactive: true,
|
||||
trapTab: false,
|
||||
closeOnScroll: false,
|
||||
modalForMobile: true,
|
||||
data: this.menuData,
|
||||
placement: "top-start",
|
||||
fallbackPlacements: ["bottom-start"],
|
||||
updateOnScroll: false,
|
||||
onClose: () => {
|
||||
this.removeHighlightedText();
|
||||
},
|
||||
post: this.args.outletArgs.post,
|
||||
selectedText: this.selectedText,
|
||||
};
|
||||
});
|
||||
|
||||
await this.currentMenu.destroy();
|
||||
|
||||
this.highlightSelectedText();
|
||||
}
|
||||
|
||||
<template>
|
||||
{{#if this.showMainButtons}}
|
||||
{{yield}}
|
||||
{{/if}}
|
||||
{{yield}}
|
||||
|
||||
{{#if this.showAiButtons}}
|
||||
<div class="ai-post-helper">
|
||||
{{#if (eq this.menuState this.MENU_STATES.triggers)}}
|
||||
<DButton
|
||||
@icon="discourse-sparkles"
|
||||
@title="discourse_ai.ai_helper.post_options_menu.title"
|
||||
@label="discourse_ai.ai_helper.post_options_menu.trigger"
|
||||
@action={{this.showAiPostHelperMenu}}
|
||||
class="btn-flat ai-post-helper__trigger"
|
||||
/>
|
||||
|
||||
{{else if (eq this.menuState this.MENU_STATES.options)}}
|
||||
<AiPostHelperMenu @data={{this.menuData}} />
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="ai-post-helper">
|
||||
<DButton
|
||||
@icon="discourse-sparkles"
|
||||
@title="discourse_ai.ai_helper.post_options_menu.title"
|
||||
@label="discourse_ai.ai_helper.post_options_menu.trigger"
|
||||
@action={{this.showAiPostHelperMenu}}
|
||||
class="btn-flat ai-post-helper__trigger"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
}
|
||||
|
||||
@@ -3,27 +3,9 @@ import { LinkTo } from "@ember/routing";
|
||||
import dIcon from "discourse/helpers/d-icon";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
function showAiPreferences(user, settings) {
|
||||
// Since we only have one AI related user setting we don't show
|
||||
// AI preferences if these conditions aren't met.
|
||||
// If we add more user settings in the future we can move this
|
||||
// logic to the the specific settings and conditionally show it in the template.
|
||||
const aiHelperEnabledFeatures =
|
||||
settings.ai_helper_enabled_features.split("|");
|
||||
|
||||
return (
|
||||
user?.user_allowed_ai_auto_image_captions &&
|
||||
aiHelperEnabledFeatures.includes("image_caption") &&
|
||||
settings.ai_helper_enabled
|
||||
);
|
||||
}
|
||||
|
||||
export default class AutoImageCaptionSetting extends Component {
|
||||
static shouldRender(outletArgs, helper) {
|
||||
return (
|
||||
helper.siteSettings.discourse_ai_enabled &&
|
||||
showAiPreferences(outletArgs.model, helper.siteSettings)
|
||||
);
|
||||
return helper.siteSettings.discourse_ai_enabled;
|
||||
}
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
class VirtualElementFromCaretCoords {
|
||||
constructor(caretCoords, offset = [0, 0]) {
|
||||
this.caretCoords = caretCoords;
|
||||
this.offset = offset;
|
||||
this.updateRect();
|
||||
}
|
||||
|
||||
updateRect() {
|
||||
const [xOffset, yOffset] = this.offset;
|
||||
this.rect = {
|
||||
top: this.caretCoords.y + yOffset,
|
||||
right: this.caretCoords.x,
|
||||
bottom: this.caretCoords.y + yOffset,
|
||||
left: this.caretCoords.x + xOffset,
|
||||
width: 0,
|
||||
height: 0,
|
||||
x: this.caretCoords.x,
|
||||
y: this.caretCoords.y,
|
||||
toJSON() {
|
||||
return this;
|
||||
},
|
||||
};
|
||||
return this.rect;
|
||||
}
|
||||
|
||||
getBoundingClientRect() {
|
||||
return this.rect;
|
||||
}
|
||||
|
||||
getClientRects() {
|
||||
return [this.rect];
|
||||
}
|
||||
|
||||
get clientWidth() {
|
||||
return this.rect.width;
|
||||
}
|
||||
|
||||
get clientHeight() {
|
||||
return this.rect.height;
|
||||
}
|
||||
}
|
||||
|
||||
export default function virtualElementFromCaretCoords(caretCoords, offset) {
|
||||
return new VirtualElementFromCaretCoords(caretCoords, offset);
|
||||
}
|
||||
@@ -84,6 +84,13 @@ export default class AiConversationsSidebarManager extends Service {
|
||||
);
|
||||
}
|
||||
|
||||
addEmptyStateClass() {
|
||||
document.body.classList.toggle(
|
||||
"has-empty-ai-conversations-sidebar",
|
||||
!this.topics.length
|
||||
);
|
||||
}
|
||||
|
||||
forceCustomSidebar() {
|
||||
document.body.classList.add("has-ai-conversations-sidebar");
|
||||
if (!this.sidebarState.isForcingSidebar) {
|
||||
@@ -100,6 +107,7 @@ export default class AiConversationsSidebarManager extends Service {
|
||||
|
||||
// don't render sidebar multiple times
|
||||
if (this._didInit) {
|
||||
this.addEmptyStateClass();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -107,6 +115,7 @@ export default class AiConversationsSidebarManager extends Service {
|
||||
|
||||
this.fetchMessages().then(() => {
|
||||
this.sidebarState.setPanel(AI_CONVERSATIONS_PANEL);
|
||||
this.addEmptyStateClass();
|
||||
});
|
||||
|
||||
return true;
|
||||
@@ -140,6 +149,7 @@ export default class AiConversationsSidebarManager extends Service {
|
||||
|
||||
stopForcingCustomSidebar() {
|
||||
document.body.classList.remove("has-ai-conversations-sidebar");
|
||||
document.body.classList.remove("has-empty-ai-conversations-sidebar");
|
||||
|
||||
const isAdmin = this.sidebarState.currentPanel?.key === ADMIN_PANEL;
|
||||
if (this.sidebarState.isForcingSidebar && !isAdmin) {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { get } from "@ember/helper";
|
||||
import RouteTemplate from "ember-route-template";
|
||||
import { eq } from "truth-helpers";
|
||||
import PreferenceCheckbox from "discourse/components/preference-checkbox";
|
||||
import SaveControls from "discourse/components/save-controls";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
export default RouteTemplate(
|
||||
<template>
|
||||
<div class="ai-user-preferences">
|
||||
<legend class="control-label">{{i18n "discourse_ai.title"}}</legend>
|
||||
|
||||
{{#each @controller.booleanSettings as |setting|}}
|
||||
{{#if setting.isIncluded}}
|
||||
<div class="control-group ai-setting">
|
||||
<PreferenceCheckbox
|
||||
@labelKey={{setting.label}}
|
||||
@checked={{get @controller.model.user_option setting.key}}
|
||||
data-setting-name={{setting.settingName}}
|
||||
class="pref-{{setting.settingName}}"
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
|
||||
{{#if (eq @controller.userSettingAttributes.length 0)}}
|
||||
{{i18n "discourse_ai.user_preferences.empty"}}
|
||||
{{/if}}
|
||||
|
||||
{{#unless (eq @controller.userSettingAttributes.length 0)}}
|
||||
<SaveControls
|
||||
@id="user_ai_preference_save"
|
||||
@model={{@controller.model}}
|
||||
@action={{@controller.save}}
|
||||
@saved={{@controller.saved}}
|
||||
/>
|
||||
{{/unless}}
|
||||
</div>
|
||||
</template>
|
||||
);
|
||||
@@ -1,29 +0,0 @@
|
||||
<div class="ai-user-preferences">
|
||||
<legend class="control-label">{{i18n "discourse_ai.title"}}</legend>
|
||||
|
||||
{{#each this.booleanSettings as |setting|}}
|
||||
{{#if setting.isIncluded}}
|
||||
<div class="control-group ai-setting">
|
||||
<PreferenceCheckbox
|
||||
@labelKey={{setting.label}}
|
||||
@checked={{get this.model.user_option setting.key}}
|
||||
data-setting-name={{setting.settingName}}
|
||||
class="pref-{{setting.settingName}}"
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
|
||||
{{#if (eq this.userSettingAttributes.length 0)}}
|
||||
{{i18n "discourse_ai.user_preferences.empty"}}
|
||||
{{/if}}
|
||||
|
||||
{{#unless (eq this.userSettingAttributes.length 0)}}
|
||||
<SaveControls
|
||||
@id="user_ai_preference_save"
|
||||
@model={{this.model}}
|
||||
@action={{this.save}}
|
||||
@saved={{this.saved}}
|
||||
/>
|
||||
{{/unless}}
|
||||
</div>
|
||||
@@ -23,8 +23,20 @@ body.has-ai-conversations-sidebar {
|
||||
}
|
||||
}
|
||||
|
||||
// we always have the "today" section rendered at the top of the sidebar but hide it when empty
|
||||
.sidebar-section[data-section-name="today"]:has(.ai-bot-sidebar-empty-state) {
|
||||
// When the sidebar is empty, we want to hide the "Today" header
|
||||
// and only show the empty state component
|
||||
&.has-empty-ai-conversations-sidebar
|
||||
.sidebar-section[data-section-name="today"]
|
||||
.sidebar-section-header-wrapper {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// When the sidebar isn't empty, BUT "today" is empty, hide
|
||||
// the entire section
|
||||
&:not(.has-empty-ai-conversations-sidebar)
|
||||
.sidebar-section[data-section-name="today"]:has(
|
||||
.ai-bot-sidebar-empty-state
|
||||
) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -125,6 +137,13 @@ body.has-ai-conversations-sidebar {
|
||||
gap: 0.5em;
|
||||
justify-content: flex-start;
|
||||
|
||||
.select-kit-body {
|
||||
.select-kit-collection {
|
||||
max-height: 50vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&__selection-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -300,6 +300,8 @@ ar:
|
||||
open_modal: "تعديل"
|
||||
modal:
|
||||
key_title: "المفتاح"
|
||||
filters:
|
||||
reset: "إعادة التعيين"
|
||||
rag:
|
||||
options:
|
||||
rag_chunk_tokens: "تحميل أجزاء الرموز"
|
||||
@@ -478,7 +480,6 @@ ar:
|
||||
trigger: "اسأل الذكاء الاصطناعي"
|
||||
loading: "الذكاء الاصطناعي قيد إنشاء المحتوى"
|
||||
cancel: "إلغاء"
|
||||
regen: "إعادة المحاولة"
|
||||
confirm: "تأكيد"
|
||||
discard: "تجاهل"
|
||||
changes: "التعديلات المقترحة"
|
||||
|
||||
@@ -60,6 +60,8 @@ be:
|
||||
delete: "выдаляць"
|
||||
response_format:
|
||||
open_modal: "рэдагаваць"
|
||||
filters:
|
||||
reset: "скінуць"
|
||||
rag:
|
||||
uploads:
|
||||
title: "Загрузкі"
|
||||
@@ -93,7 +95,6 @@ be:
|
||||
ai_helper:
|
||||
context_menu:
|
||||
cancel: "адмяніць"
|
||||
regen: "Паспрабаваць яшчэ раз"
|
||||
post_options_menu:
|
||||
close: "зачыніць"
|
||||
copy: "капіяваць"
|
||||
|
||||
@@ -71,6 +71,8 @@ bg:
|
||||
open_modal: "Редактирай"
|
||||
modal:
|
||||
key_title: "Ключ"
|
||||
filters:
|
||||
reset: "Нулиране"
|
||||
rag:
|
||||
uploads:
|
||||
uploading: "Качва се..."
|
||||
@@ -111,7 +113,6 @@ bg:
|
||||
ai_helper:
|
||||
context_menu:
|
||||
cancel: "Прекрати"
|
||||
regen: "Опитайте отново"
|
||||
discard: "Отхвърляне"
|
||||
post_options_menu:
|
||||
close: "Затвори"
|
||||
|
||||
@@ -66,6 +66,8 @@ bs_BA:
|
||||
open_modal: "Edit"
|
||||
modal:
|
||||
key_title: "Ključ"
|
||||
filters:
|
||||
reset: "Resetovati"
|
||||
rag:
|
||||
uploads:
|
||||
title: "Uploads"
|
||||
@@ -101,7 +103,6 @@ bs_BA:
|
||||
ai_helper:
|
||||
context_menu:
|
||||
cancel: "Odustani"
|
||||
regen: "Pokušaj ponovo"
|
||||
discard: "Odbaci"
|
||||
post_options_menu:
|
||||
close: "Zatvori"
|
||||
|
||||
@@ -69,6 +69,8 @@ ca:
|
||||
open_modal: "Edita"
|
||||
modal:
|
||||
key_title: "Clau"
|
||||
filters:
|
||||
reset: "Restableix"
|
||||
rag:
|
||||
uploads:
|
||||
title: "Càrregues"
|
||||
@@ -105,7 +107,6 @@ ca:
|
||||
ai_helper:
|
||||
context_menu:
|
||||
cancel: "Cancel·la"
|
||||
regen: "Torna-ho a intentar"
|
||||
confirm: "Confirma"
|
||||
discard: "Descarta"
|
||||
post_options_menu:
|
||||
|
||||
@@ -79,6 +79,8 @@ cs:
|
||||
open_modal: "Upravit"
|
||||
modal:
|
||||
key_title: "Klíč"
|
||||
filters:
|
||||
reset: "obnovit výchozí"
|
||||
rag:
|
||||
uploads:
|
||||
title: "Nahrané soubory"
|
||||
@@ -152,7 +154,6 @@ cs:
|
||||
trigger: "Zeptejte se AI"
|
||||
loading: "Umělá inteligence generuje"
|
||||
cancel: "Zrušit"
|
||||
regen: "Zkusit znovu"
|
||||
confirm: "Potvrdit"
|
||||
discard: "Zrušit"
|
||||
changes: "Navrhované úpravy"
|
||||
|
||||
@@ -74,6 +74,8 @@ da:
|
||||
open_modal: "Rediger"
|
||||
modal:
|
||||
key_title: "Nøgle"
|
||||
filters:
|
||||
reset: "Nulstil"
|
||||
rag:
|
||||
uploads:
|
||||
title: "Overførsler"
|
||||
@@ -117,7 +119,6 @@ da:
|
||||
ai_helper:
|
||||
context_menu:
|
||||
cancel: "Annuller"
|
||||
regen: "Prøv igen"
|
||||
confirm: "Bekræft"
|
||||
discard: "Kassér"
|
||||
post_options_menu:
|
||||
|
||||
@@ -353,6 +353,12 @@ de:
|
||||
ai_bot:
|
||||
title: "KI-Bot-Optionen"
|
||||
save_first: "Weitere KI-Bot-Optionen werden verfügbar, sobald du die Persona gespeichert hast."
|
||||
filters:
|
||||
reset: "Zurücksetzen"
|
||||
features_list:
|
||||
one: "Funktion:"
|
||||
other: "Funktionen:"
|
||||
llms_list: "LLM:"
|
||||
rag:
|
||||
title: "RAG"
|
||||
options:
|
||||
@@ -562,7 +568,6 @@ de:
|
||||
trigger: "KI fragen"
|
||||
loading: "KI generiert"
|
||||
cancel: "Abbrechen"
|
||||
regen: "Erneut versuchen"
|
||||
confirm: "Bestätigen"
|
||||
discard: "Verwerfen"
|
||||
changes: "Empfohlene Änderungen"
|
||||
|
||||
@@ -73,6 +73,8 @@ el:
|
||||
open_modal: "Επεξεργασία"
|
||||
modal:
|
||||
key_title: "Κλειδί"
|
||||
filters:
|
||||
reset: "Επαναφορά"
|
||||
rag:
|
||||
uploads:
|
||||
title: "Μεταφορτώσεις"
|
||||
@@ -116,7 +118,6 @@ el:
|
||||
ai_helper:
|
||||
context_menu:
|
||||
cancel: "Ακύρωση"
|
||||
regen: "Δοκίμασε ξανά"
|
||||
discard: "Απόρριψη"
|
||||
post_options_menu:
|
||||
close: "Κλείσιμο"
|
||||
|
||||
@@ -539,14 +539,16 @@ en:
|
||||
|
||||
model_description:
|
||||
none: "General settings that work for most language models"
|
||||
anthropic-claude-3-7-sonnet: "Anthropic's most intelligent model"
|
||||
anthropic-claude-3-5-haiku: "Fast and cost-effective"
|
||||
anthropic-claude-3-opus: "Excels at writing and complex tasks"
|
||||
google-gemini-2-5-pro: "Mid-sized multimodal model capable of a wide range of tasks"
|
||||
google-gemini-2-0-flash: "Lightweight, fast, and cost-efficient with multimodal reasoning"
|
||||
anthropic-claude-opus-4-0: "Anthropic's most intelligent model"
|
||||
anthropic-claude-sonnet-4-0: "Optimal balance of speed and cost"
|
||||
anthropic-claude-3-7-sonnet-latest: "Optimal balance of speed and cost (previous generation)"
|
||||
anthropic-claude-3-5-haiku-latest: "Fast and cost-effective"
|
||||
google-gemini-2-5-pro: "Large multimodal model capable of a wide range of tasks"
|
||||
google-gemini-2-0-flash: "Lightweight, fast, and cost-efficient with multimodal reasoning (previous generation)"
|
||||
google-gemini-2-5-flash: "Lightweight, fast, and cost-efficient with multimodal reasoning"
|
||||
google-gemini-2-0-flash-lite: "Cost efficient and low latency model"
|
||||
open_ai-o1: "Open AI's most capable reasoning model"
|
||||
open_ai-o3-mini: "Advanced Cost-efficient reasoning model"
|
||||
open_ai-o3: "Open AI's most capable reasoning model"
|
||||
open_ai-o4-mini: "Advanced Cost-efficient reasoning model"
|
||||
open_ai-gpt-4-1: "Open AI's flagship model. It is well suited for problem solving across domains"
|
||||
open_ai-gpt-4-1-mini: "Provides a balance between intelligence, speed, and cost that makes it an attractive model for many use cases."
|
||||
open_ai-gpt-4-1-nano: "Fastest, most cost-effective GPT-4.1 model."
|
||||
@@ -554,6 +556,9 @@ en:
|
||||
samba_nova-Meta-Llama-3-3-70B-Instruct": "Powerful multipurpose model"
|
||||
mistral-mistral-large-latest: "Mistral's most powerful model"
|
||||
mistral-pixtral-large-latest: "Mistral's most powerful vision capable model"
|
||||
open_router-x-ai-grok-3-beta: "xAI's latest model"
|
||||
open_router-deepseek-deepseek-r1-0528-free: "DeepSeek's latest reasoning model"
|
||||
open_router-meta-llama-3-3-70b-instruct: "Highly capable multilingual model"
|
||||
|
||||
preseeded_model_description: "Pre-configured open-source model utilizing %{model}"
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user