2023-11-03 07:30:09 -04:00
|
|
|
import Component from "@glimmer/component";
|
2023-09-01 20:10:58 -04:00
|
|
|
import { tracked } from "@glimmer/tracking";
|
2024-01-12 18:28:06 -05:00
|
|
|
import { fn } from "@ember/helper";
|
2023-09-01 20:10:58 -04:00
|
|
|
import { action } from "@ember/object";
|
2023-11-03 07:30:09 -04:00
|
|
|
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
|
|
|
import { inject as service } from "@ember/service";
|
|
|
|
import DButton from "discourse/components/d-button";
|
2023-09-01 20:10:58 -04:00
|
|
|
import { ajax } from "discourse/lib/ajax";
|
|
|
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
|
|
|
import { bind } from "discourse-common/utils/decorators";
|
|
|
|
import I18n from "I18n";
|
|
|
|
|
|
|
|
export default class AISuggestionDropdown extends Component {
|
|
|
|
@service dialog;
|
|
|
|
@service siteSettings;
|
2023-09-06 15:20:08 -04:00
|
|
|
@service composer;
|
2024-01-12 18:28:06 -05:00
|
|
|
|
2023-09-01 20:10:58 -04:00
|
|
|
@tracked loading = false;
|
|
|
|
@tracked showMenu = false;
|
|
|
|
@tracked generatedSuggestions = [];
|
|
|
|
@tracked suggestIcon = "discourse-sparkles";
|
|
|
|
@tracked showErrors = false;
|
|
|
|
@tracked error = "";
|
2024-01-12 18:28:06 -05:00
|
|
|
|
2023-09-01 20:10:58 -04:00
|
|
|
SUGGESTION_TYPES = {
|
|
|
|
title: "suggest_title",
|
|
|
|
category: "suggest_category",
|
|
|
|
tag: "suggest_tags",
|
|
|
|
};
|
|
|
|
|
|
|
|
willDestroy() {
|
|
|
|
super.willDestroy(...arguments);
|
|
|
|
document.removeEventListener("click", this.onClickOutside);
|
|
|
|
}
|
|
|
|
|
2023-09-06 15:20:08 -04:00
|
|
|
get showAIButton() {
|
|
|
|
const minCharacterCount = 40;
|
2023-11-03 14:41:57 -04:00
|
|
|
const isShowAIButton = this.composer.model.replyLength > minCharacterCount;
|
|
|
|
const composerFields = document.querySelector(".composer-fields");
|
FEATURE: UI to update ai personas on admin page (#290)
Introduces a UI to manage customizable personas (admin only feature)
Part of the change was some extensive internal refactoring:
- AIBot now has a persona set in the constructor, once set it never changes
- Command now takes in bot as a constructor param, so it has the correct persona and is not generating AIBot objects on the fly
- Added a .prettierignore file, due to the way ALE is configured in nvim it is a pre-req for prettier to work
- Adds a bunch of validations on the AIPersona model, system personas (artist/creative etc...) are all seeded. We now ensure
- name uniqueness, and only allow certain properties to be touched for system personas.
- (JS note) the client side design takes advantage of nested routes, the parent route for personas gets all the personas via this.store.findAll("ai-persona") then child routes simply reach into this model to find a particular persona.
- (JS note) data is sideloaded into the ai-persona model the meta property supplied from the controller, resultSetMeta
- This removes ai_bot_enabled_personas and ai_bot_enabled_chat_commands, both should be controlled from the UI on a per persona basis
- Fixes a long standing bug in token accounting ... we were doing to_json.length instead of to_json.to_s.length
- Amended it so {commands} are always inserted at the end unconditionally, no need to add it to the template of the system message as it just confuses things
- Adds a concept of required_commands to stock personas, these are commands that must be configured for this stock persona to show up.
- Refactored tests so we stop requiring inference_stubs, it was very confusing to need it, added to plugin.rb for now which at least is clearer
- Migrates the persona selector to gjs
---------
Co-authored-by: Joffrey JAFFEUX <j.jaffeux@gmail.com>
Co-authored-by: Martin Brennan <martin@discourse.org>
2023-11-21 00:56:43 -05:00
|
|
|
|
2023-11-03 14:41:57 -04:00
|
|
|
if (composerFields) {
|
|
|
|
if (isShowAIButton) {
|
|
|
|
composerFields.classList.add("showing-ai-suggestions");
|
|
|
|
} else {
|
|
|
|
composerFields.classList.remove("showing-ai-suggestions");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return isShowAIButton;
|
2023-09-01 20:10:58 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
get disableSuggestionButton() {
|
|
|
|
return this.loading;
|
|
|
|
}
|
|
|
|
|
2023-11-03 14:41:57 -04:00
|
|
|
@action
|
|
|
|
applyClasses() {
|
|
|
|
if (this.showAIButton) {
|
2023-11-29 17:01:48 -05:00
|
|
|
document
|
|
|
|
.querySelector(".composer-fields")
|
|
|
|
?.classList.add("showing-ai-suggestions");
|
2023-11-03 14:41:57 -04:00
|
|
|
} else {
|
2023-11-29 17:01:48 -05:00
|
|
|
document
|
|
|
|
.querySelector(".composer-fields")
|
|
|
|
?.classList.remove("showing-ai-suggestions");
|
2023-11-03 14:41:57 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-01 20:10:58 -04:00
|
|
|
@bind
|
|
|
|
onClickOutside(event) {
|
|
|
|
const menu = document.querySelector(".ai-title-suggestions-menu");
|
|
|
|
|
|
|
|
if (event.target === menu) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-09-06 15:20:08 -04:00
|
|
|
return this.#closeMenu();
|
2023-09-01 20:10:58 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
@action
|
|
|
|
handleClickOutside() {
|
|
|
|
document.addEventListener("click", this.onClickOutside);
|
|
|
|
}
|
|
|
|
|
|
|
|
@action
|
|
|
|
applySuggestion(suggestion) {
|
|
|
|
if (!this.args.mode) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const composer = this.args?.composer;
|
|
|
|
if (!composer) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.args.mode === this.SUGGESTION_TYPES.title) {
|
|
|
|
composer.set("title", suggestion);
|
2023-09-06 15:20:08 -04:00
|
|
|
return this.#closeMenu();
|
2023-09-01 20:10:58 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
if (this.args.mode === this.SUGGESTION_TYPES.category) {
|
2023-11-03 07:30:09 -04:00
|
|
|
const selectedCategoryId = this.composer.categories.find(
|
|
|
|
(c) => c.slug === suggestion
|
|
|
|
).id;
|
|
|
|
composer.set("categoryId", selectedCategoryId);
|
|
|
|
return this.#closeMenu();
|
2023-09-01 20:10:58 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
if (this.args.mode === this.SUGGESTION_TYPES.tag) {
|
2023-09-06 15:20:08 -04:00
|
|
|
this.#updateTags(suggestion, composer);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@action
|
|
|
|
async performSuggestion() {
|
|
|
|
if (!this.args.mode) {
|
|
|
|
return;
|
2023-09-01 20:10:58 -04:00
|
|
|
}
|
2023-09-06 15:20:08 -04:00
|
|
|
|
|
|
|
if (this.composer.model.replyLength === 0) {
|
2023-11-03 07:30:09 -04:00
|
|
|
return this.dialog.alert(
|
|
|
|
I18n.t("discourse_ai.ai_helper.missing_content")
|
|
|
|
);
|
2023-09-06 15:20:08 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
this.loading = true;
|
|
|
|
this.suggestIcon = "spinner";
|
|
|
|
|
|
|
|
return ajax(`/discourse-ai/ai-helper/${this.args.mode}`, {
|
|
|
|
method: "POST",
|
|
|
|
data: { text: this.composer.model.reply },
|
2023-11-03 07:30:09 -04:00
|
|
|
})
|
|
|
|
.then((data) => {
|
|
|
|
this.#assignGeneratedSuggestions(data, this.args.mode);
|
|
|
|
})
|
|
|
|
.catch(popupAjaxError)
|
|
|
|
.finally(() => {
|
|
|
|
this.loading = false;
|
|
|
|
this.suggestIcon = "sync-alt";
|
|
|
|
this.showMenu = true;
|
2023-11-03 14:41:57 -04:00
|
|
|
|
|
|
|
if (this.args.mode === "suggest_category") {
|
2023-11-29 17:01:48 -05:00
|
|
|
document
|
|
|
|
.querySelector(".category-input")
|
|
|
|
?.classList.add("showing-ai-suggestion-menu");
|
2023-11-03 14:41:57 -04:00
|
|
|
}
|
2023-11-03 07:30:09 -04:00
|
|
|
});
|
2023-09-06 15:20:08 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
#closeMenu() {
|
2023-11-03 14:41:57 -04:00
|
|
|
if (this.showMenu && this.args.mode === "suggest_category") {
|
2023-11-29 17:01:48 -05:00
|
|
|
document
|
|
|
|
.querySelector(".category-input")
|
|
|
|
?.classList.remove("showing-ai-suggestion-menu");
|
2023-11-03 14:41:57 -04:00
|
|
|
}
|
|
|
|
|
2023-09-06 15:20:08 -04:00
|
|
|
this.suggestIcon = "discourse-sparkles";
|
|
|
|
this.showMenu = false;
|
|
|
|
this.showErrors = false;
|
|
|
|
this.errors = "";
|
2023-09-01 20:10:58 -04:00
|
|
|
}
|
|
|
|
|
2023-09-06 15:20:08 -04:00
|
|
|
#updateTags(suggestion, composer) {
|
2023-09-01 20:10:58 -04:00
|
|
|
const maxTags = this.siteSettings.max_tags_per_topic;
|
|
|
|
|
|
|
|
if (!composer.tags) {
|
|
|
|
composer.set("tags", [suggestion]);
|
|
|
|
// remove tag from the list of suggestions once added
|
2023-11-03 07:30:09 -04:00
|
|
|
this.generatedSuggestions = this.generatedSuggestions.filter(
|
|
|
|
(s) => s !== suggestion
|
|
|
|
);
|
2023-09-01 20:10:58 -04:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
const tags = composer.tags;
|
|
|
|
|
|
|
|
if (tags?.length >= maxTags) {
|
|
|
|
// Show error if trying to add more tags than allowed
|
|
|
|
this.showErrors = true;
|
2023-11-03 07:30:09 -04:00
|
|
|
this.error = I18n.t("select_kit.max_content_reached", { count: maxTags });
|
2023-09-01 20:10:58 -04:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
tags.push(suggestion);
|
|
|
|
composer.set("tags", [...tags]);
|
|
|
|
// remove tag from the list of suggestions once added
|
2023-11-03 07:30:09 -04:00
|
|
|
return (this.generatedSuggestions = this.generatedSuggestions.filter(
|
|
|
|
(s) => s !== suggestion
|
|
|
|
));
|
2023-09-01 20:10:58 -04:00
|
|
|
}
|
|
|
|
|
2023-09-06 15:20:08 -04:00
|
|
|
#tagSelectorHasValues() {
|
|
|
|
return this.args.composer?.tags && this.args.composer?.tags.length > 0;
|
|
|
|
}
|
2023-09-01 20:10:58 -04:00
|
|
|
|
2023-09-06 15:20:08 -04:00
|
|
|
#assignGeneratedSuggestions(data, mode) {
|
|
|
|
if (mode === this.SUGGESTION_TYPES.title) {
|
2023-11-03 07:30:09 -04:00
|
|
|
return (this.generatedSuggestions = data.suggestions);
|
2023-09-01 20:10:58 -04:00
|
|
|
}
|
|
|
|
|
2023-09-06 15:20:08 -04:00
|
|
|
const suggestions = data.assistant.map((s) => s.name);
|
2023-09-01 20:10:58 -04:00
|
|
|
|
2023-09-06 15:20:08 -04:00
|
|
|
if (mode === this.SUGGESTION_TYPES.tag) {
|
|
|
|
if (this.#tagSelectorHasValues()) {
|
|
|
|
// Filter out tags if they are already selected in the tag input
|
2023-11-03 07:30:09 -04:00
|
|
|
return (this.generatedSuggestions = suggestions.filter(
|
|
|
|
(t) => !this.args.composer.tags.includes(t)
|
|
|
|
));
|
2023-09-01 20:10:58 -04:00
|
|
|
} else {
|
2023-11-03 07:30:09 -04:00
|
|
|
return (this.generatedSuggestions = suggestions);
|
2023-09-01 20:10:58 -04:00
|
|
|
}
|
2023-09-06 15:20:08 -04:00
|
|
|
}
|
2023-09-01 20:10:58 -04:00
|
|
|
|
2023-11-03 07:30:09 -04:00
|
|
|
return (this.generatedSuggestions = suggestions);
|
2023-09-01 20:10:58 -04:00
|
|
|
}
|
2023-11-03 07:30:09 -04:00
|
|
|
|
|
|
|
<template>
|
|
|
|
{{#if this.showAIButton}}
|
|
|
|
<DButton
|
|
|
|
@icon={{this.suggestIcon}}
|
|
|
|
@title="discourse_ai.ai_helper.suggest"
|
|
|
|
@action={{this.performSuggestion}}
|
|
|
|
@disabled={{this.disableSuggestionButton}}
|
2024-01-12 18:28:06 -05:00
|
|
|
class="suggestion-button {{if this.loading 'is-loading'}}"
|
2023-11-03 07:30:09 -04:00
|
|
|
...attributes
|
|
|
|
/>
|
|
|
|
{{/if}}
|
|
|
|
|
|
|
|
{{#if this.showMenu}}
|
|
|
|
{{! template-lint-disable modifier-name-case }}
|
|
|
|
<ul
|
|
|
|
class="popup-menu ai-suggestions-menu"
|
|
|
|
{{didInsert this.handleClickOutside}}
|
|
|
|
>
|
|
|
|
{{#if this.showErrors}}
|
|
|
|
<li class="ai-suggestions-menu__errors">{{this.error}}</li>
|
|
|
|
{{/if}}
|
|
|
|
{{#each this.generatedSuggestions as |suggestion index|}}
|
|
|
|
<li data-name={{suggestion}} data-value={{index}}>
|
|
|
|
<DButton
|
|
|
|
@translatedLabel={{suggestion}}
|
2024-01-12 18:28:06 -05:00
|
|
|
@action={{fn this.applySuggestion suggestion}}
|
|
|
|
class="popup-menu-btn"
|
2023-11-03 07:30:09 -04:00
|
|
|
/>
|
|
|
|
</li>
|
|
|
|
{{/each}}
|
|
|
|
</ul>
|
|
|
|
{{/if}}
|
|
|
|
</template>
|
|
|
|
}
|