mirror of
https://github.com/discourse/discourse-ai.git
synced 2025-06-27 01:52:18 +00:00
REFACTOR: Helper suggestions (#914)
This PR adds some updates to the Helper suggestions to improve it's functionality and modernize some of the codebase.
This commit is contained in:
parent
0d3e6b2726
commit
6b7d7c1179
@ -54,8 +54,13 @@ export default class AiSplitTopicSuggester extends Component {
|
|||||||
suggestions.includes(item.name.toLowerCase())
|
suggestions.includes(item.name.toLowerCase())
|
||||||
);
|
);
|
||||||
this.suggestions = suggestedCategories;
|
this.suggestions = suggestedCategories;
|
||||||
} else {
|
} else if (this.args.mode === this.SUGGESTION_TYPES.tag) {
|
||||||
this.suggestions = result.assistant.map((s) => s.name);
|
this.suggestions = result.assistant.map((s) => {
|
||||||
|
return {
|
||||||
|
name: s.name,
|
||||||
|
count: s.count,
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(popupAjaxError)
|
.catch(popupAjaxError)
|
||||||
@ -132,6 +137,17 @@ export default class AiSplitTopicSuggester extends Component {
|
|||||||
{{on "click" (fn this.applySuggestion suggestion menu)}}
|
{{on "click" (fn this.applySuggestion suggestion menu)}}
|
||||||
>
|
>
|
||||||
{{categoryBadge suggestion}}
|
{{categoryBadge suggestion}}
|
||||||
|
<span class="topic-count">x
|
||||||
|
{{suggestion.totalTopicCount}}</span>
|
||||||
|
</li>
|
||||||
|
{{else if (eq @mode "suggest_tags")}}
|
||||||
|
<li data-name={{suggestion.name}} data-value={{index}}>
|
||||||
|
<DButton
|
||||||
|
@translatedLabel={{suggestion.name}}
|
||||||
|
@action={{fn this.applySuggestion suggestion.name menu}}
|
||||||
|
>
|
||||||
|
<span class="topic-count">x{{suggestion.count}}</span>
|
||||||
|
</DButton>
|
||||||
</li>
|
</li>
|
||||||
{{else}}
|
{{else}}
|
||||||
<li data-name={{suggestion}} data-value={{index}}>
|
<li data-name={{suggestion}} data-value={{index}}>
|
||||||
|
@ -1,249 +0,0 @@
|
|||||||
import Component from "@glimmer/component";
|
|
||||||
import { tracked } from "@glimmer/tracking";
|
|
||||||
import { fn } from "@ember/helper";
|
|
||||||
import { action } from "@ember/object";
|
|
||||||
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
|
||||||
import { service } from "@ember/service";
|
|
||||||
import DButton from "discourse/components/d-button";
|
|
||||||
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;
|
|
||||||
@service composer;
|
|
||||||
|
|
||||||
@tracked loading = false;
|
|
||||||
@tracked showMenu = false;
|
|
||||||
@tracked generatedSuggestions = [];
|
|
||||||
@tracked suggestIcon = "discourse-sparkles";
|
|
||||||
@tracked showErrors = false;
|
|
||||||
@tracked error = "";
|
|
||||||
|
|
||||||
SUGGESTION_TYPES = {
|
|
||||||
title: "suggest_title",
|
|
||||||
category: "suggest_category",
|
|
||||||
tag: "suggest_tags",
|
|
||||||
};
|
|
||||||
|
|
||||||
willDestroy() {
|
|
||||||
super.willDestroy(...arguments);
|
|
||||||
document.removeEventListener("click", this.onClickOutside);
|
|
||||||
}
|
|
||||||
|
|
||||||
get showAIButton() {
|
|
||||||
const minCharacterCount = 40;
|
|
||||||
const isShowAIButton = this.composer.model.replyLength > minCharacterCount;
|
|
||||||
const composerFields = document.querySelector(".composer-fields");
|
|
||||||
|
|
||||||
if (composerFields) {
|
|
||||||
if (isShowAIButton) {
|
|
||||||
composerFields.classList.add("showing-ai-suggestions");
|
|
||||||
} else {
|
|
||||||
composerFields.classList.remove("showing-ai-suggestions");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return isShowAIButton;
|
|
||||||
}
|
|
||||||
|
|
||||||
get disableSuggestionButton() {
|
|
||||||
return this.loading;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
applyClasses() {
|
|
||||||
if (this.showAIButton) {
|
|
||||||
document
|
|
||||||
.querySelector(".composer-fields")
|
|
||||||
?.classList.add("showing-ai-suggestions");
|
|
||||||
} else {
|
|
||||||
document
|
|
||||||
.querySelector(".composer-fields")
|
|
||||||
?.classList.remove("showing-ai-suggestions");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@bind
|
|
||||||
onClickOutside(event) {
|
|
||||||
const menu = document.querySelector(".ai-title-suggestions-menu");
|
|
||||||
|
|
||||||
if (event.target === menu) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.#closeMenu();
|
|
||||||
}
|
|
||||||
|
|
||||||
@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);
|
|
||||||
return this.#closeMenu();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.args.mode === this.SUGGESTION_TYPES.category) {
|
|
||||||
const selectedCategoryId = this.composer.categories.find(
|
|
||||||
(c) => c.slug === suggestion
|
|
||||||
).id;
|
|
||||||
composer.set("categoryId", selectedCategoryId);
|
|
||||||
return this.#closeMenu();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.args.mode === this.SUGGESTION_TYPES.tag) {
|
|
||||||
this.#updateTags(suggestion, composer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
async performSuggestion() {
|
|
||||||
if (!this.args.mode) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.composer.model.replyLength === 0) {
|
|
||||||
return this.dialog.alert(
|
|
||||||
I18n.t("discourse_ai.ai_helper.missing_content")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loading = true;
|
|
||||||
this.suggestIcon = "spinner";
|
|
||||||
|
|
||||||
return ajax(`/discourse-ai/ai-helper/${this.args.mode}`, {
|
|
||||||
method: "POST",
|
|
||||||
data: { text: this.composer.model.reply },
|
|
||||||
})
|
|
||||||
.then((data) => {
|
|
||||||
this.#assignGeneratedSuggestions(data, this.args.mode);
|
|
||||||
})
|
|
||||||
.catch(popupAjaxError)
|
|
||||||
.finally(() => {
|
|
||||||
this.loading = false;
|
|
||||||
this.suggestIcon = "sync-alt";
|
|
||||||
this.showMenu = true;
|
|
||||||
|
|
||||||
if (this.args.mode === "suggest_category") {
|
|
||||||
document
|
|
||||||
.querySelector(".category-input")
|
|
||||||
?.classList.add("showing-ai-suggestion-menu");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#closeMenu() {
|
|
||||||
if (this.showMenu && this.args.mode === "suggest_category") {
|
|
||||||
document
|
|
||||||
.querySelector(".category-input")
|
|
||||||
?.classList.remove("showing-ai-suggestion-menu");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.suggestIcon = "discourse-sparkles";
|
|
||||||
this.showMenu = false;
|
|
||||||
this.showErrors = false;
|
|
||||||
this.errors = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
#updateTags(suggestion, composer) {
|
|
||||||
const maxTags = this.siteSettings.max_tags_per_topic;
|
|
||||||
|
|
||||||
if (!composer.tags) {
|
|
||||||
composer.set("tags", [suggestion]);
|
|
||||||
// remove tag from the list of suggestions once added
|
|
||||||
this.generatedSuggestions = this.generatedSuggestions.filter(
|
|
||||||
(s) => s !== suggestion
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const tags = composer.tags;
|
|
||||||
|
|
||||||
if (tags?.length >= maxTags) {
|
|
||||||
// Show error if trying to add more tags than allowed
|
|
||||||
this.showErrors = true;
|
|
||||||
this.error = I18n.t("select_kit.max_content_reached", { count: maxTags });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
tags.push(suggestion);
|
|
||||||
composer.set("tags", [...tags]);
|
|
||||||
// remove tag from the list of suggestions once added
|
|
||||||
return (this.generatedSuggestions = this.generatedSuggestions.filter(
|
|
||||||
(s) => s !== suggestion
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
#tagSelectorHasValues() {
|
|
||||||
return this.args.composer?.tags && this.args.composer?.tags.length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#assignGeneratedSuggestions(data, mode) {
|
|
||||||
if (mode === this.SUGGESTION_TYPES.title) {
|
|
||||||
return (this.generatedSuggestions = data.suggestions);
|
|
||||||
}
|
|
||||||
|
|
||||||
const suggestions = data.assistant.map((s) => s.name);
|
|
||||||
|
|
||||||
if (mode === this.SUGGESTION_TYPES.tag) {
|
|
||||||
if (this.#tagSelectorHasValues()) {
|
|
||||||
// Filter out tags if they are already selected in the tag input
|
|
||||||
return (this.generatedSuggestions = suggestions.filter(
|
|
||||||
(t) => !this.args.composer.tags.includes(t)
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
return (this.generatedSuggestions = suggestions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (this.generatedSuggestions = suggestions);
|
|
||||||
}
|
|
||||||
|
|
||||||
<template>
|
|
||||||
{{#if this.showAIButton}}
|
|
||||||
<DButton
|
|
||||||
@icon={{this.suggestIcon}}
|
|
||||||
@title="discourse_ai.ai_helper.suggest"
|
|
||||||
@action={{this.performSuggestion}}
|
|
||||||
@disabled={{this.disableSuggestionButton}}
|
|
||||||
class="suggestion-button {{if this.loading 'is-loading'}}"
|
|
||||||
...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}}
|
|
||||||
@action={{fn this.applySuggestion suggestion}}
|
|
||||||
class="popup-menu-btn"
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
{{/each}}
|
|
||||||
</ul>
|
|
||||||
{{/if}}
|
|
||||||
</template>
|
|
||||||
}
|
|
@ -0,0 +1,154 @@
|
|||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
import { fn } from "@ember/helper";
|
||||||
|
import { on } from "@ember/modifier";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
import { service } from "@ember/service";
|
||||||
|
import DButton from "discourse/components/d-button";
|
||||||
|
import DropdownMenu from "discourse/components/dropdown-menu";
|
||||||
|
import categoryBadge from "discourse/helpers/category-badge";
|
||||||
|
import { ajax } from "discourse/lib/ajax";
|
||||||
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
|
import i18n from "discourse-common/helpers/i18n";
|
||||||
|
import DMenu from "float-kit/components/d-menu";
|
||||||
|
import { MIN_CHARACTER_COUNT } from "../../lib/ai-helper-suggestions";
|
||||||
|
|
||||||
|
export default class AiCategorySuggester extends Component {
|
||||||
|
@service siteSettings;
|
||||||
|
@tracked loading = false;
|
||||||
|
@tracked suggestions = null;
|
||||||
|
@tracked untriggers = [];
|
||||||
|
@tracked triggerIcon = "discourse-sparkles";
|
||||||
|
@tracked content = null;
|
||||||
|
@tracked topicContent = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
if (!this.topicContent && this.args.composer?.reply === undefined) {
|
||||||
|
this.fetchTopicContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchTopicContent() {
|
||||||
|
await ajax(`/t/${this.args.buffered.content.id}.json`).then(
|
||||||
|
({ post_stream }) => {
|
||||||
|
this.topicContent = post_stream.posts[0].cooked;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get showSuggestionButton() {
|
||||||
|
const composerFields = document.querySelector(".composer-fields");
|
||||||
|
this.content = this.args.composer?.reply || this.topicContent;
|
||||||
|
const showTrigger = this.content?.length > MIN_CHARACTER_COUNT;
|
||||||
|
|
||||||
|
if (composerFields) {
|
||||||
|
if (showTrigger) {
|
||||||
|
composerFields.classList.add("showing-ai-suggestions");
|
||||||
|
} else {
|
||||||
|
composerFields.classList.remove("showing-ai-suggestions");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.siteSettings.ai_embeddings_enabled && showTrigger;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
async loadSuggestions() {
|
||||||
|
if (this.suggestions && !this.dMenu.expanded) {
|
||||||
|
return this.suggestions;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
this.triggerIcon = "spinner";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { assistant } = await ajax(
|
||||||
|
"/discourse-ai/ai-helper/suggest_category",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
data: { text: this.content },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this.suggestions = assistant;
|
||||||
|
} catch (error) {
|
||||||
|
popupAjaxError(error);
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
this.triggerIcon = "sync-alt";
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.suggestions;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
applySuggestion(suggestion) {
|
||||||
|
const composer = this.args.composer;
|
||||||
|
const buffered = this.args.buffered;
|
||||||
|
|
||||||
|
if (composer) {
|
||||||
|
composer.set("categoryId", suggestion.id);
|
||||||
|
composer.get("categoryId");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buffered) {
|
||||||
|
this.args.buffered.set("category_id", suggestion.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.dMenu.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
onRegisterApi(api) {
|
||||||
|
this.dMenu = api;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
onClose() {
|
||||||
|
this.triggerIcon = "discourse-sparkles";
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
{{#if this.showSuggestionButton}}
|
||||||
|
<DMenu
|
||||||
|
@title={{i18n "discourse_ai.ai_helper.suggest"}}
|
||||||
|
@icon={{this.triggerIcon}}
|
||||||
|
@identifier="ai-category-suggester"
|
||||||
|
@onClose={{this.onClose}}
|
||||||
|
@triggerClass="suggestion-button suggest-category-button {{if
|
||||||
|
this.loading
|
||||||
|
'is-loading'
|
||||||
|
}}"
|
||||||
|
@contentClass="ai-suggestions-menu"
|
||||||
|
@onRegisterApi={{this.onRegisterApi}}
|
||||||
|
@modalForMobile={{true}}
|
||||||
|
@untriggers={{this.untriggers}}
|
||||||
|
{{on "click" this.loadSuggestions}}
|
||||||
|
>
|
||||||
|
<:content>
|
||||||
|
{{#unless this.loading}}
|
||||||
|
<DropdownMenu as |dropdown|>
|
||||||
|
{{#each this.suggestions as |suggestion index|}}
|
||||||
|
<dropdown.item>
|
||||||
|
<DButton
|
||||||
|
class="category-row"
|
||||||
|
data-name={{suggestion.name}}
|
||||||
|
data-value={{index}}
|
||||||
|
title={{suggestion.name}}
|
||||||
|
@action={{fn this.applySuggestion suggestion}}
|
||||||
|
>
|
||||||
|
<div class="category-status">
|
||||||
|
{{categoryBadge suggestion}}
|
||||||
|
<span class="topic-count">x
|
||||||
|
{{suggestion.topicCount}}</span>
|
||||||
|
</div>
|
||||||
|
</DButton>
|
||||||
|
</dropdown.item>
|
||||||
|
{{/each}}
|
||||||
|
</DropdownMenu>
|
||||||
|
{{/unless}}
|
||||||
|
</:content>
|
||||||
|
</DMenu>
|
||||||
|
{{/if}}
|
||||||
|
</template>
|
||||||
|
}
|
@ -0,0 +1,218 @@
|
|||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
import { fn } from "@ember/helper";
|
||||||
|
import { on } from "@ember/modifier";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
import { service } from "@ember/service";
|
||||||
|
import DButton from "discourse/components/d-button";
|
||||||
|
import DropdownMenu from "discourse/components/dropdown-menu";
|
||||||
|
import discourseTag from "discourse/helpers/discourse-tag";
|
||||||
|
import { ajax } from "discourse/lib/ajax";
|
||||||
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
|
import i18n from "discourse-common/helpers/i18n";
|
||||||
|
import DMenu from "float-kit/components/d-menu";
|
||||||
|
import { MIN_CHARACTER_COUNT } from "../../lib/ai-helper-suggestions";
|
||||||
|
|
||||||
|
export default class AiTagSuggester extends Component {
|
||||||
|
@service siteSettings;
|
||||||
|
@service toasts;
|
||||||
|
@tracked loading = false;
|
||||||
|
@tracked suggestions = null;
|
||||||
|
@tracked untriggers = [];
|
||||||
|
@tracked triggerIcon = "discourse-sparkles";
|
||||||
|
@tracked content = null;
|
||||||
|
@tracked topicContent = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
if (!this.topicContent && this.args.composer?.reply === undefined) {
|
||||||
|
this.fetchTopicContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchTopicContent() {
|
||||||
|
await ajax(`/t/${this.args.buffered.content.id}.json`).then(
|
||||||
|
({ post_stream }) => {
|
||||||
|
this.topicContent = post_stream.posts[0].cooked;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get showSuggestionButton() {
|
||||||
|
const composerFields = document.querySelector(".composer-fields");
|
||||||
|
this.content = this.args.composer?.reply || this.topicContent;
|
||||||
|
const showTrigger = this.content?.length > MIN_CHARACTER_COUNT;
|
||||||
|
|
||||||
|
if (composerFields) {
|
||||||
|
if (showTrigger) {
|
||||||
|
composerFields.classList.add("showing-ai-suggestions");
|
||||||
|
} else {
|
||||||
|
composerFields.classList.remove("showing-ai-suggestions");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.siteSettings.ai_embeddings_enabled && showTrigger;
|
||||||
|
}
|
||||||
|
|
||||||
|
get showDropdown() {
|
||||||
|
if (this.suggestions?.length <= 0) {
|
||||||
|
this.dMenu.close();
|
||||||
|
}
|
||||||
|
return !this.loading && this.suggestions?.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
async loadSuggestions() {
|
||||||
|
if (
|
||||||
|
this.suggestions &&
|
||||||
|
this.suggestions?.length > 0 &&
|
||||||
|
!this.dMenu.expanded
|
||||||
|
) {
|
||||||
|
return this.suggestions;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
this.triggerIcon = "spinner";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { assistant } = await ajax("/discourse-ai/ai-helper/suggest_tags", {
|
||||||
|
method: "POST",
|
||||||
|
data: { text: this.content },
|
||||||
|
});
|
||||||
|
this.suggestions = assistant;
|
||||||
|
const model = this.args.composer
|
||||||
|
? this.args.composer
|
||||||
|
: this.args.buffered;
|
||||||
|
if (this.#tagSelectorHasValues()) {
|
||||||
|
this.suggestions = this.suggestions.filter(
|
||||||
|
(s) => !model.get("tags").includes(s.name)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.suggestions?.length <= 0) {
|
||||||
|
this.toasts.error({
|
||||||
|
class: "ai-suggestion-error",
|
||||||
|
duration: 3000,
|
||||||
|
data: {
|
||||||
|
message: i18n(
|
||||||
|
"discourse_ai.ai_helper.suggest_errors.no_suggestions"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
popupAjaxError(error);
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
this.triggerIcon = "sync-alt";
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.suggestions;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tagSelectorHasValues() {
|
||||||
|
return this.args.composer?.tags && this.args.composer?.tags.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#removedAppliedTag(suggestion) {
|
||||||
|
return (this.suggestions = this.suggestions.filter(
|
||||||
|
(s) => s.id !== suggestion.id
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
applySuggestion(suggestion) {
|
||||||
|
const maxTags = this.siteSettings.max_tags_per_topic;
|
||||||
|
const model = this.args.composer ? this.args.composer : this.args.buffered;
|
||||||
|
if (!model) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tags = model.get("tags");
|
||||||
|
|
||||||
|
if (!tags) {
|
||||||
|
model.set("tags", [suggestion.name]);
|
||||||
|
this.#removedAppliedTag(suggestion);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tags?.length >= maxTags) {
|
||||||
|
return this.toasts.error({
|
||||||
|
class: "ai-suggestion-error",
|
||||||
|
duration: 3000,
|
||||||
|
data: {
|
||||||
|
message: i18n("discourse_ai.ai_helper.suggest_errors.too_many_tags", {
|
||||||
|
count: maxTags,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
tags.push(suggestion.name);
|
||||||
|
model.set("tags", [...tags]);
|
||||||
|
suggestion.disabled = true;
|
||||||
|
this.#removedAppliedTag(suggestion);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
onRegisterApi(api) {
|
||||||
|
this.dMenu = api;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
onClose() {
|
||||||
|
if (this.suggestions?.length > 0) {
|
||||||
|
// If all suggestions have been used,
|
||||||
|
// re-triggering when no suggestions present
|
||||||
|
// will cause computation issues with
|
||||||
|
// setting the icon, so we prevent it
|
||||||
|
this.triggerIcon = "discourse-sparkles";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
{{#if this.showSuggestionButton}}
|
||||||
|
<DMenu
|
||||||
|
@title={{i18n "discourse_ai.ai_helper.suggest"}}
|
||||||
|
@icon={{this.triggerIcon}}
|
||||||
|
@identifier="ai-tag-suggester"
|
||||||
|
@onClose={{this.onClose}}
|
||||||
|
@triggerClass="suggestion-button suggest-tags-button {{if
|
||||||
|
this.loading
|
||||||
|
'is-loading'
|
||||||
|
}}"
|
||||||
|
@contentClass="ai-suggestions-menu"
|
||||||
|
@onRegisterApi={{this.onRegisterApi}}
|
||||||
|
@modalForMobile={{true}}
|
||||||
|
@untriggers={{this.untriggers}}
|
||||||
|
{{on "click" this.loadSuggestions}}
|
||||||
|
>
|
||||||
|
<:content>
|
||||||
|
{{#if this.showDropdown}}
|
||||||
|
<DropdownMenu as |dropdown|>
|
||||||
|
{{#each this.suggestions as |suggestion index|}}
|
||||||
|
<dropdown.item>
|
||||||
|
<DButton
|
||||||
|
class="tag-row"
|
||||||
|
data-name={{suggestion.name}}
|
||||||
|
data-value={{index}}
|
||||||
|
title={{suggestion.name}}
|
||||||
|
@disabled={{this.isDisabled suggestion}}
|
||||||
|
@action={{fn this.applySuggestion suggestion}}
|
||||||
|
>
|
||||||
|
{{discourseTag
|
||||||
|
suggestion.name
|
||||||
|
count=suggestion.count
|
||||||
|
noHref=true
|
||||||
|
}}
|
||||||
|
</DButton>
|
||||||
|
</dropdown.item>
|
||||||
|
{{/each}}
|
||||||
|
</DropdownMenu>
|
||||||
|
{{/if}}
|
||||||
|
</:content>
|
||||||
|
</DMenu>
|
||||||
|
{{/if}}
|
||||||
|
</template>
|
||||||
|
}
|
@ -0,0 +1,151 @@
|
|||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
import { fn } from "@ember/helper";
|
||||||
|
import { on } from "@ember/modifier";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
import DButton from "discourse/components/d-button";
|
||||||
|
import DropdownMenu from "discourse/components/dropdown-menu";
|
||||||
|
import { ajax } from "discourse/lib/ajax";
|
||||||
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
|
import i18n from "discourse-common/helpers/i18n";
|
||||||
|
import DMenu from "float-kit/components/d-menu";
|
||||||
|
import { MIN_CHARACTER_COUNT } from "../../lib/ai-helper-suggestions";
|
||||||
|
|
||||||
|
export default class AiTitleSuggester extends Component {
|
||||||
|
@tracked loading = false;
|
||||||
|
@tracked suggestions = null;
|
||||||
|
@tracked untriggers = [];
|
||||||
|
@tracked triggerIcon = "discourse-sparkles";
|
||||||
|
@tracked content = null;
|
||||||
|
@tracked topicContent = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
|
||||||
|
if (!this.topicContent && this.args.composer?.reply === undefined) {
|
||||||
|
this.fetchTopicContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchTopicContent() {
|
||||||
|
await ajax(`/t/${this.args.buffered.content.id}.json`).then(
|
||||||
|
({ post_stream }) => {
|
||||||
|
this.topicContent = post_stream.posts[0].cooked;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get showSuggestionButton() {
|
||||||
|
const composerFields = document.querySelector(".composer-fields");
|
||||||
|
const editTopicTitleField = document.querySelector(".edit-topic-title");
|
||||||
|
|
||||||
|
this.content = this.args.composer?.reply || this.topicContent;
|
||||||
|
const showTrigger = this.content?.length > MIN_CHARACTER_COUNT;
|
||||||
|
|
||||||
|
if (composerFields) {
|
||||||
|
if (showTrigger) {
|
||||||
|
composerFields.classList.add("showing-ai-suggestions");
|
||||||
|
} else {
|
||||||
|
composerFields.classList.remove("showing-ai-suggestions");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editTopicTitleField) {
|
||||||
|
if (showTrigger) {
|
||||||
|
editTopicTitleField.classList.add("showing-ai-suggestions");
|
||||||
|
} else {
|
||||||
|
editTopicTitleField.classList.remove("showing-ai-suggestions");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return showTrigger;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
async loadSuggestions() {
|
||||||
|
if (this.suggestions && !this.dMenu.expanded) {
|
||||||
|
return this.suggestions;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
this.triggerIcon = "spinner";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { suggestions } = await ajax(
|
||||||
|
"/discourse-ai/ai-helper/suggest_title",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
data: { text: this.content },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this.suggestions = suggestions;
|
||||||
|
} catch (error) {
|
||||||
|
popupAjaxError(error);
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
this.triggerIcon = "sync-alt";
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.suggestions;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
applySuggestion(suggestion) {
|
||||||
|
const model = this.args.composer ? this.args.composer : this.args.buffered;
|
||||||
|
if (!model) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
model.set("title", suggestion);
|
||||||
|
this.dMenu.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
onRegisterApi(api) {
|
||||||
|
this.dMenu = api;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
onClose() {
|
||||||
|
this.triggerIcon = "discourse-sparkles";
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
{{#if this.showSuggestionButton}}
|
||||||
|
<DMenu
|
||||||
|
@title={{i18n "discourse_ai.ai_helper.suggest"}}
|
||||||
|
@icon={{this.triggerIcon}}
|
||||||
|
@identifier="ai-title-suggester"
|
||||||
|
@onClose={{this.onClose}}
|
||||||
|
@triggerClass="suggestion-button suggest-titles-button {{if
|
||||||
|
this.loading
|
||||||
|
'is-loading'
|
||||||
|
}}"
|
||||||
|
@contentClass="ai-suggestions-menu"
|
||||||
|
@onRegisterApi={{this.onRegisterApi}}
|
||||||
|
@modalForMobile={{true}}
|
||||||
|
@untriggers={{this.untriggers}}
|
||||||
|
{{on "click" this.loadSuggestions}}
|
||||||
|
>
|
||||||
|
<:content>
|
||||||
|
{{#unless this.loading}}
|
||||||
|
<DropdownMenu as |dropdown|>
|
||||||
|
{{#each this.suggestions as |suggestion index|}}
|
||||||
|
<dropdown.item>
|
||||||
|
<DButton
|
||||||
|
data-name={{suggestion}}
|
||||||
|
data-value={{index}}
|
||||||
|
title={{suggestion}}
|
||||||
|
@action={{fn this.applySuggestion suggestion}}
|
||||||
|
>
|
||||||
|
{{suggestion}}
|
||||||
|
</DButton>
|
||||||
|
</dropdown.item>
|
||||||
|
{{/each}}
|
||||||
|
</DropdownMenu>
|
||||||
|
{{/unless}}
|
||||||
|
</:content>
|
||||||
|
</DMenu>
|
||||||
|
{{/if}}
|
||||||
|
</template>
|
||||||
|
}
|
@ -1,6 +1,5 @@
|
|||||||
import Component from "@glimmer/component";
|
import Component from "@glimmer/component";
|
||||||
import { service } from "@ember/service";
|
import AiCategorySuggester from "../../components/suggestion-menus/ai-category-suggester";
|
||||||
import AISuggestionDropdown from "../../components/ai-suggestion-dropdown";
|
|
||||||
import { showComposerAiHelper } from "../../lib/show-ai-helper";
|
import { showComposerAiHelper } from "../../lib/show-ai-helper";
|
||||||
|
|
||||||
export default class AiCategorySuggestion extends Component {
|
export default class AiCategorySuggestion extends Component {
|
||||||
@ -13,15 +12,7 @@ export default class AiCategorySuggestion extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@service siteSettings;
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
{{#if this.siteSettings.ai_embeddings_enabled}}
|
<AiCategorySuggester @composer={{@outletArgs.composer}} />
|
||||||
<AISuggestionDropdown
|
|
||||||
@mode="suggest_category"
|
|
||||||
@composer={{@outletArgs.composer}}
|
|
||||||
class="suggest-category-button"
|
|
||||||
/>
|
|
||||||
{{/if}}
|
|
||||||
</template>
|
</template>
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import Component from "@glimmer/component";
|
import Component from "@glimmer/component";
|
||||||
import { service } from "@ember/service";
|
import AiTagSuggester from "../../components/suggestion-menus/ai-tag-suggester";
|
||||||
import AISuggestionDropdown from "../../components/ai-suggestion-dropdown";
|
|
||||||
import { showComposerAiHelper } from "../../lib/show-ai-helper";
|
import { showComposerAiHelper } from "../../lib/show-ai-helper";
|
||||||
|
|
||||||
export default class AiTagSuggestion extends Component {
|
export default class AiTagSuggestion extends Component {
|
||||||
@ -13,15 +12,7 @@ export default class AiTagSuggestion extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@service siteSettings;
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
{{#if this.siteSettings.ai_embeddings_enabled}}
|
<AiTagSuggester @composer={{@outletArgs.composer}} />
|
||||||
<AISuggestionDropdown
|
|
||||||
@mode="suggest_tags"
|
|
||||||
@composer={{@outletArgs.composer}}
|
|
||||||
class="suggest-tags-button"
|
|
||||||
/>
|
|
||||||
{{/if}}
|
|
||||||
</template>
|
</template>
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import Component from "@glimmer/component";
|
import Component from "@glimmer/component";
|
||||||
import AISuggestionDropdown from "../../components/ai-suggestion-dropdown";
|
import AiTitleSuggester from "../../components/suggestion-menus/ai-title-suggester";
|
||||||
import { showComposerAiHelper } from "../../lib/show-ai-helper";
|
import { showComposerAiHelper } from "../../lib/show-ai-helper";
|
||||||
|
|
||||||
export default class AiTitleSuggestion extends Component {
|
export default class AiTitleSuggestion extends Component {
|
||||||
@ -13,10 +13,6 @@ export default class AiTitleSuggestion extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AISuggestionDropdown
|
<AiTitleSuggester @composer={{@outletArgs.composer}} />
|
||||||
@mode="suggest_title"
|
|
||||||
@composer={{@outletArgs.composer}}
|
|
||||||
class="suggest-titles-button"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
import Component from "@glimmer/component";
|
||||||
|
import AiCategorySuggester from "../../components/suggestion-menus/ai-category-suggester";
|
||||||
|
import { showComposerAiHelper } from "../../lib/show-ai-helper";
|
||||||
|
|
||||||
|
export default class AiCategorySuggestion extends Component {
|
||||||
|
static shouldRender(outletArgs, helper) {
|
||||||
|
return showComposerAiHelper(
|
||||||
|
outletArgs?.composer,
|
||||||
|
helper.siteSettings,
|
||||||
|
helper.currentUser,
|
||||||
|
"suggestions"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AiCategorySuggester @buffered={{@outletArgs.buffered}} />
|
||||||
|
</template>
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
import Component from "@glimmer/component";
|
||||||
|
import AiTagSuggester from "../../components/suggestion-menus/ai-tag-suggester";
|
||||||
|
import { showComposerAiHelper } from "../../lib/show-ai-helper";
|
||||||
|
|
||||||
|
export default class AiCategorySuggestion extends Component {
|
||||||
|
static shouldRender(outletArgs, helper) {
|
||||||
|
return showComposerAiHelper(
|
||||||
|
outletArgs?.composer,
|
||||||
|
helper.siteSettings,
|
||||||
|
helper.currentUser,
|
||||||
|
"suggestions"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AiTagSuggester @buffered={{@outletArgs.buffered}} />
|
||||||
|
</template>
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
import Component from "@glimmer/component";
|
||||||
|
import AiTitleSuggester from "../../components/suggestion-menus/ai-title-suggester";
|
||||||
|
import { showComposerAiHelper } from "../../lib/show-ai-helper";
|
||||||
|
|
||||||
|
export default class AiTitleSuggestion extends Component {
|
||||||
|
static shouldRender(outletArgs, helper) {
|
||||||
|
return showComposerAiHelper(
|
||||||
|
outletArgs?.composer,
|
||||||
|
helper.siteSettings,
|
||||||
|
helper.currentUser,
|
||||||
|
"suggestions"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AiTitleSuggester @buffered={{@outletArgs.buffered}} />
|
||||||
|
</template>
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export const MIN_CHARACTER_COUNT = 40;
|
@ -228,25 +228,50 @@
|
|||||||
border-bottom-left-radius: 0;
|
border-bottom-left-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ai-suggestions-menu {
|
.ai-category-suggester-content,
|
||||||
list-style: none;
|
.ai-tag-suggester-content,
|
||||||
margin-left: 0;
|
.ai-title-suggester-content {
|
||||||
position: absolute;
|
z-index: z("composer", "dropdown");
|
||||||
right: 0;
|
}
|
||||||
top: 1.5rem;
|
|
||||||
max-width: 25rem;
|
|
||||||
width: unset;
|
|
||||||
z-index: 999;
|
|
||||||
|
|
||||||
&__errors {
|
.ai-category-suggester-content {
|
||||||
background: var(--danger);
|
.category-row {
|
||||||
padding: 0.25rem 1em;
|
padding: 0.25em 0.5em;
|
||||||
color: var(--secondary);
|
color: var(--primary-high);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--d-hover);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-input.showing-ai-suggestion-menu {
|
.topic-count {
|
||||||
position: relative;
|
font-size: var(--font-down-2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-tag-suggester-content {
|
||||||
|
.tag-row {
|
||||||
|
.discourse-tag-count {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-topic-title {
|
||||||
|
.suggestion-button {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.45rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#topic-title .edit-topic-title.showing-ai-suggestions {
|
||||||
|
#edit-title {
|
||||||
|
flex: 1 1 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggest-titles-button {
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent suggestion button from wrapping
|
// Prevent suggestion button from wrapping
|
||||||
@ -393,6 +418,11 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.topic-count {
|
||||||
|
font-size: var(--font-down-2);
|
||||||
|
color: var(--primary-high);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.fk-d-menu[data-identifier="ai-split-topic-suggestion-menu"] {
|
.fk-d-menu[data-identifier="ai-split-topic-suggestion-menu"] {
|
||||||
|
@ -344,6 +344,11 @@ en:
|
|||||||
description: "Choose one of the options below, and the AI will suggest you a new version of the text."
|
description: "Choose one of the options below, and the AI will suggest you a new version of the text."
|
||||||
selection_hint: "Hint: You can also select a portion of the text before opening the helper to rewrite only that."
|
selection_hint: "Hint: You can also select a portion of the text before opening the helper to rewrite only that."
|
||||||
suggest: "Suggest with AI"
|
suggest: "Suggest with AI"
|
||||||
|
suggest_errors:
|
||||||
|
too_many_tags:
|
||||||
|
one: "You can only have up to %{count} tag"
|
||||||
|
other: "You can only have up to %{count} tags"
|
||||||
|
no_suggestions: "No suggestions available"
|
||||||
missing_content: "Please enter some content to generate suggestions."
|
missing_content: "Please enter some content to generate suggestions."
|
||||||
context_menu:
|
context_menu:
|
||||||
trigger: "Ask AI"
|
trigger: "Ask AI"
|
||||||
|
@ -19,15 +19,30 @@ module DiscourseAi
|
|||||||
.where(id: candidate_ids)
|
.where(id: candidate_ids)
|
||||||
.where("categories.id IN (?)", Category.topic_create_allowed(@user.guardian).pluck(:id))
|
.where("categories.id IN (?)", Category.topic_create_allowed(@user.guardian).pluck(:id))
|
||||||
.order("array_position(ARRAY#{candidate_ids}, topics.id)")
|
.order("array_position(ARRAY#{candidate_ids}, topics.id)")
|
||||||
.pluck("categories.slug")
|
.pluck(
|
||||||
|
"categories.id",
|
||||||
|
"categories.name",
|
||||||
|
"categories.slug",
|
||||||
|
"categories.color",
|
||||||
|
"categories.topic_count",
|
||||||
|
)
|
||||||
.map
|
.map
|
||||||
.with_index { |category, index| { name: category, score: candidates[index].last } }
|
.with_index do |(id, name, slug, color, topic_count), index|
|
||||||
|
{
|
||||||
|
id: id,
|
||||||
|
name: name,
|
||||||
|
slug: slug,
|
||||||
|
color: color,
|
||||||
|
topicCount: topic_count,
|
||||||
|
score: candidates[index].last,
|
||||||
|
}
|
||||||
|
end
|
||||||
.map do |c|
|
.map do |c|
|
||||||
c[:score] = 1 / (c[:score] + 1) # inverse of the distance
|
c[:score] = 1 / (c[:score] + 1) # inverse of the distance
|
||||||
c
|
c
|
||||||
end
|
end
|
||||||
.group_by { |c| c[:name] }
|
.group_by { |c| c[:name] }
|
||||||
.map { |name, scores| { name: name, score: scores.sum { |s| s[:score] } } }
|
.map { |name, scores| scores.first.merge(score: scores.sum { |s| s[:score] }) }
|
||||||
.sort_by { |c| -c[:score] }
|
.sort_by { |c| -c[:score] }
|
||||||
.take(5)
|
.take(5)
|
||||||
end
|
end
|
||||||
@ -39,24 +54,35 @@ module DiscourseAi
|
|||||||
candidates = nearest_neighbors(limit: 100)
|
candidates = nearest_neighbors(limit: 100)
|
||||||
candidate_ids = candidates.map(&:first)
|
candidate_ids = candidates.map(&:first)
|
||||||
|
|
||||||
|
count_column = Tag.topic_count_column(@user.guardian) # Determine the count column
|
||||||
|
|
||||||
::Topic
|
::Topic
|
||||||
.joins(:topic_tags, :tags)
|
.joins(:topic_tags, :tags)
|
||||||
.where(id: candidate_ids)
|
.where(id: candidate_ids)
|
||||||
.where("tags.id IN (?)", DiscourseTagging.visible_tags(@user.guardian).pluck(:id))
|
.where("tags.id IN (?)", DiscourseTagging.visible_tags(@user.guardian).pluck(:id))
|
||||||
.group("topics.id")
|
.group("topics.id, tags.id, tags.name") # Group by topics.id and tags.id
|
||||||
.order("array_position(ARRAY#{candidate_ids}, topics.id)")
|
.order("array_position(ARRAY#{candidate_ids}, topics.id)")
|
||||||
.pluck("array_agg(tags.name)")
|
.pluck(
|
||||||
.map(&:uniq)
|
"tags.id",
|
||||||
|
"tags.name",
|
||||||
|
"tags.#{count_column}",
|
||||||
|
"MIN(array_position(ARRAY#{candidate_ids}, topics.id))", # Get minimum index for ordering
|
||||||
|
)
|
||||||
|
.uniq # Ensure unique tags per topic
|
||||||
.map
|
.map
|
||||||
.with_index { |tag_list, index| { tags: tag_list, score: candidates[index].last } }
|
.with_index do |(id, name, count, index), idx|
|
||||||
.flat_map { |c| c[:tags].map { |t| { name: t, score: c[:score] } } }
|
{
|
||||||
.map do |c|
|
id: id,
|
||||||
c[:score] = 1 / (c[:score] + 1) # inverse of the distance
|
name: name,
|
||||||
c
|
count: count,
|
||||||
|
score: 1 / (candidates[idx].last + 1), # Inverse of the distance for score
|
||||||
|
}
|
||||||
end
|
end
|
||||||
.group_by { |c| c[:name] }
|
.group_by { |tag| tag[:name] }
|
||||||
.map { |name, scores| { name: name, score: scores.sum { |s| s[:score] } } }
|
.map do |name, tags|
|
||||||
.sort_by { |c| -c[:score] }
|
tags.first.merge(score: tags.sum { |t| t[:score] })
|
||||||
|
end # Aggregate scores per tag
|
||||||
|
.sort_by { |tag| -tag[:score] }
|
||||||
.take(5)
|
.take(5)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ RSpec.describe "AI Composer helper", type: :system, js: true do
|
|||||||
let(:composer) { PageObjects::Components::Composer.new }
|
let(:composer) { PageObjects::Components::Composer.new }
|
||||||
let(:ai_helper_menu) { PageObjects::Components::AiComposerHelperMenu.new }
|
let(:ai_helper_menu) { PageObjects::Components::AiComposerHelperMenu.new }
|
||||||
let(:diff_modal) { PageObjects::Modals::DiffModal.new }
|
let(:diff_modal) { PageObjects::Modals::DiffModal.new }
|
||||||
let(:ai_suggestion_dropdown) { PageObjects::Components::AISuggestionDropdown.new }
|
let(:ai_suggestion_dropdown) { PageObjects::Components::AiSuggestionDropdown.new }
|
||||||
let(:toasts) { PageObjects::Components::Toasts.new }
|
let(:toasts) { PageObjects::Components::Toasts.new }
|
||||||
|
|
||||||
fab!(:category)
|
fab!(:category)
|
||||||
@ -236,20 +236,26 @@ RSpec.describe "AI Composer helper", type: :system, js: true do
|
|||||||
response =
|
response =
|
||||||
Category
|
Category
|
||||||
.take(3)
|
.take(3)
|
||||||
.pluck(:slug)
|
.map do |category|
|
||||||
.map { |s| { name: s, score: rand(0.0...45.0) } }
|
{
|
||||||
.sort { |h| h[:score] }
|
id: category.id,
|
||||||
|
name: category.name,
|
||||||
|
slug: category.slug,
|
||||||
|
color: category.color,
|
||||||
|
score: rand(0.0...45.0),
|
||||||
|
topicCount: rand(1..3),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
.sort_by { |h| h[:score] }
|
||||||
DiscourseAi::AiHelper::SemanticCategorizer.any_instance.stubs(:categories).returns(response)
|
DiscourseAi::AiHelper::SemanticCategorizer.any_instance.stubs(:categories).returns(response)
|
||||||
visit("/latest")
|
visit("/latest")
|
||||||
page.find("#create-topic").click
|
page.find("#create-topic").click
|
||||||
composer.fill_content(input)
|
composer.fill_content(input)
|
||||||
ai_suggestion_dropdown.click_suggest_category_button
|
ai_suggestion_dropdown.click_suggest_category_button
|
||||||
wait_for { ai_suggestion_dropdown.has_dropdown? }
|
wait_for { ai_suggestion_dropdown.has_dropdown? }
|
||||||
|
|
||||||
suggestion = category_2.name
|
suggestion = category_2.name
|
||||||
ai_suggestion_dropdown.select_suggestion_by_name(category_2.slug)
|
ai_suggestion_dropdown.select_suggestion_by_name(suggestion)
|
||||||
category_selector = page.find(".category-chooser summary")
|
category_selector = page.find(".category-chooser summary")
|
||||||
|
|
||||||
expect(category_selector["data-name"]).to eq(suggestion)
|
expect(category_selector["data-name"]).to eq(suggestion)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
module PageObjects
|
module PageObjects
|
||||||
module Components
|
module Components
|
||||||
class AISuggestionDropdown < PageObjects::Components::Base
|
class AiSuggestionDropdown < PageObjects::Components::Base
|
||||||
SUGGESTION_BUTTON_SELECTOR = ".suggestion-button"
|
SUGGESTION_BUTTON_SELECTOR = ".suggestion-button"
|
||||||
TITLE_BUTTON_SELECTOR = "#{SUGGESTION_BUTTON_SELECTOR}.suggest-titles-button"
|
TITLE_BUTTON_SELECTOR = "#{SUGGESTION_BUTTON_SELECTOR}.suggest-titles-button"
|
||||||
CATEGORY_BUTTON_SELECTOR = "#{SUGGESTION_BUTTON_SELECTOR}.suggest-category-button"
|
CATEGORY_BUTTON_SELECTOR = "#{SUGGESTION_BUTTON_SELECTOR}.suggest-category-button"
|
||||||
@ -22,15 +22,15 @@ module PageObjects
|
|||||||
end
|
end
|
||||||
|
|
||||||
def select_suggestion_by_value(index)
|
def select_suggestion_by_value(index)
|
||||||
find("#{MENU_SELECTOR} li[data-value=\"#{index}\"]").click
|
find("#{MENU_SELECTOR} button[data-value=\"#{index}\"]").click
|
||||||
end
|
end
|
||||||
|
|
||||||
def select_suggestion_by_name(name)
|
def select_suggestion_by_name(name)
|
||||||
find("#{MENU_SELECTOR} li[data-name=\"#{name}\"]").click
|
find("#{MENU_SELECTOR} button[data-name=\"#{name}\"]").click
|
||||||
end
|
end
|
||||||
|
|
||||||
def suggestion_name(index)
|
def suggestion_name(index)
|
||||||
suggestion = find("#{MENU_SELECTOR} li[data-value=\"#{index}\"]")
|
suggestion = find("#{MENU_SELECTOR} button[data-value=\"#{index}\"]")
|
||||||
suggestion["data-name"]
|
suggestion["data-name"]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user