From 69d14866393937f911ce159f1976cc5dcb5295b5 Mon Sep 17 00:00:00 2001 From: Keegan George Date: Thu, 14 Nov 2024 10:04:07 -0800 Subject: [PATCH 1/8] WIP --- .../components/ai-suggestion-dropdown.gjs | 2 + .../ai-category-suggester.gjs | 142 +++++++++++++ .../ai-category-suggestion.gjs | 13 +- .../ai-tag-suggestion.gjs | 198 +++++++++++++++++- .../ai-category-suggestion.gjs | 23 ++ .../modules/ai-helper/common/ai-helper.scss | 37 ++-- config/locales/client.en.yml | 5 + lib/ai_helper/semantic_categorizer.rb | 54 +++-- 8 files changed, 427 insertions(+), 47 deletions(-) create mode 100644 assets/javascripts/discourse/components/suggestion-menus/ai-category-suggester.gjs create mode 100644 assets/javascripts/discourse/connectors/edit-topic-category__after/ai-category-suggestion.gjs diff --git a/assets/javascripts/discourse/components/ai-suggestion-dropdown.gjs b/assets/javascripts/discourse/components/ai-suggestion-dropdown.gjs index b92abd663..df6dd195f 100644 --- a/assets/javascripts/discourse/components/ai-suggestion-dropdown.gjs +++ b/assets/javascripts/discourse/components/ai-suggestion-dropdown.gjs @@ -131,6 +131,7 @@ export default class AISuggestionDropdown extends Component { data: { text: this.composer.model.reply }, }) .then((data) => { + console.log(data); this.#assignGeneratedSuggestions(data, this.args.mode); }) .catch(popupAjaxError) @@ -198,6 +199,7 @@ export default class AISuggestionDropdown extends Component { } const suggestions = data.assistant.map((s) => s.name); + // console.log("suggest", suggestions); if (mode === this.SUGGESTION_TYPES.tag) { if (this.#tagSelectorHasValues()) { diff --git a/assets/javascripts/discourse/components/suggestion-menus/ai-category-suggester.gjs b/assets/javascripts/discourse/components/suggestion-menus/ai-category-suggester.gjs new file mode 100644 index 000000000..5b04097df --- /dev/null +++ b/assets/javascripts/discourse/components/suggestion-menus/ai-category-suggester.gjs @@ -0,0 +1,142 @@ +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 { inject as 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"; + +export default class AiCategorySuggester extends Component { + @service siteSettings; + @tracked loading = false; + @tracked suggestions = null; + @tracked untriggers = []; + @tracked triggerIcon = "discourse-sparkles"; + + get referenceText() { + if (this.args.composer?.reply) { + return this.args.composer.reply; + } + + console.log(this.args); + ajax(`/raw/${this.args.topic.id}/1.json`).then((response) => { + console.log(response); + }); + + return "abcdefhg"; + } + + get showSuggestionButton() { + const MIN_CHARACTER_COUNT = 40; + const composerFields = document.querySelector(".composer-fields"); + const showTrigger = this.referenceText.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.args.composer.reply }, + } + ); + 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; + if (!composer) { + return; + } + + composer.set("categoryId", suggestion.id); + this.dMenu.close(); + } + + @action + onRegisterApi(api) { + this.dMenu = api; + } + + @action + onClose() { + this.triggerIcon = "discourse-sparkles"; + } + + +} diff --git a/assets/javascripts/discourse/connectors/after-composer-category-input/ai-category-suggestion.gjs b/assets/javascripts/discourse/connectors/after-composer-category-input/ai-category-suggestion.gjs index f8f97c579..d7bef6428 100644 --- a/assets/javascripts/discourse/connectors/after-composer-category-input/ai-category-suggestion.gjs +++ b/assets/javascripts/discourse/connectors/after-composer-category-input/ai-category-suggestion.gjs @@ -1,6 +1,5 @@ import Component from "@glimmer/component"; -import { inject as service } from "@ember/service"; -import AISuggestionDropdown from "../../components/ai-suggestion-dropdown"; +import AiCategorySuggester from "../../components/suggestion-menus/ai-category-suggester"; import { showComposerAiHelper } from "../../lib/show-ai-helper"; export default class AiCategorySuggestion extends Component { @@ -13,15 +12,7 @@ export default class AiCategorySuggestion extends Component { ); } - @service siteSettings; - } diff --git a/assets/javascripts/discourse/connectors/after-composer-tag-input/ai-tag-suggestion.gjs b/assets/javascripts/discourse/connectors/after-composer-tag-input/ai-tag-suggestion.gjs index d07488005..21ce47bf0 100644 --- a/assets/javascripts/discourse/connectors/after-composer-tag-input/ai-tag-suggestion.gjs +++ b/assets/javascripts/discourse/connectors/after-composer-tag-input/ai-tag-suggestion.gjs @@ -1,6 +1,16 @@ 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 { inject as service } from "@ember/service"; -import AISuggestionDropdown from "../../components/ai-suggestion-dropdown"; +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 { showComposerAiHelper } from "../../lib/show-ai-helper"; export default class AiTagSuggestion extends Component { @@ -14,14 +24,188 @@ export default class AiTagSuggestion extends Component { } @service siteSettings; + @service toasts; + @tracked loading = false; + @tracked suggestions = null; + @tracked untriggers = []; + @tracked triggerIcon = "discourse-sparkles"; + + get showSuggestionButton() { + const MIN_CHARACTER_COUNT = 40; + const composerFields = document.querySelector(".composer-fields"); + const showTrigger = + this.args.outletArgs.composer.reply?.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.args.outletArgs.composer.reply }, + }); + this.suggestions = assistant; + + if (this.#tagSelectorHasValues()) { + this.suggestions = this.suggestions.filter( + (s) => !this.args.outletArgs.composer.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.outletArgs.composer?.tags && + this.args.outletArgs.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 composer = this.args.outletArgs.composer; + if (!composer) { + return; + } + + if (!composer.tags) { + composer.set("tags", [suggestion.name]); + this.#removedAppliedTag(suggestion); + return; + } + + const tags = composer.tags; + + 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); + composer.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"; + } + } } diff --git a/assets/javascripts/discourse/connectors/edit-topic-category__after/ai-category-suggestion.gjs b/assets/javascripts/discourse/connectors/edit-topic-category__after/ai-category-suggestion.gjs new file mode 100644 index 000000000..379345b0d --- /dev/null +++ b/assets/javascripts/discourse/connectors/edit-topic-category__after/ai-category-suggestion.gjs @@ -0,0 +1,23 @@ +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" + ); + } + + +} diff --git a/assets/stylesheets/modules/ai-helper/common/ai-helper.scss b/assets/stylesheets/modules/ai-helper/common/ai-helper.scss index 8c8d12656..f01d54881 100644 --- a/assets/stylesheets/modules/ai-helper/common/ai-helper.scss +++ b/assets/stylesheets/modules/ai-helper/common/ai-helper.scss @@ -228,25 +228,32 @@ border-bottom-left-radius: 0; } -.ai-suggestions-menu { - list-style: none; - margin-left: 0; - position: absolute; - right: 0; - top: 1.5rem; - max-width: 25rem; - width: unset; - z-index: 999; +.ai-category-suggester-content, +.ai-tag-suggester-content { + z-index: z("composer", "dropdown"); +} + +.ai-category-suggester-content { + .category-row { + padding: 0.25em 0.5em; + color: var(--primary-high); + + &:hover { + background: var(--d-hover); + } + } - &__errors { - background: var(--danger); - padding: 0.25rem 1em; - color: var(--secondary); + .topic-count { + font-size: var(--font-down-2); } } -.category-input.showing-ai-suggestion-menu { - position: relative; +.ai-tag-suggester-content { + .tag-row { + .discourse-tag-count { + margin-left: 5px; + } + } } // Prevent suggestion button from wrapping diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 22c5f38fe..613e4b04a 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -340,6 +340,11 @@ en: 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." 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." context_menu: trigger: "Ask AI" diff --git a/lib/ai_helper/semantic_categorizer.rb b/lib/ai_helper/semantic_categorizer.rb index b759c6c3d..7b6f953f8 100644 --- a/lib/ai_helper/semantic_categorizer.rb +++ b/lib/ai_helper/semantic_categorizer.rb @@ -19,15 +19,30 @@ def categories .where(id: candidate_ids) .where("categories.id IN (?)", Category.topic_create_allowed(@user.guardian).pluck(: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 - .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| c[:score] = 1 / (c[:score] + 1) # inverse of the distance c end .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] } .take(5) end @@ -39,24 +54,35 @@ def tags candidates = nearest_neighbors(limit: 100) candidate_ids = candidates.map(&:first) + count_column = Tag.topic_count_column(@user.guardian) # Determine the count column + ::Topic .joins(:topic_tags, :tags) .where(id: candidate_ids) .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)") - .pluck("array_agg(tags.name)") - .map(&:uniq) + .pluck( + "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 - .with_index { |tag_list, index| { tags: tag_list, score: candidates[index].last } } - .flat_map { |c| c[:tags].map { |t| { name: t, score: c[:score] } } } - .map do |c| - c[:score] = 1 / (c[:score] + 1) # inverse of the distance - c + .with_index do |(id, name, count, index), idx| + { + id: id, + name: name, + count: count, + score: 1 / (candidates[idx].last + 1), # Inverse of the distance for score + } end - .group_by { |c| c[:name] } - .map { |name, scores| { name: name, score: scores.sum { |s| s[:score] } } } - .sort_by { |c| -c[:score] } + .group_by { |tag| tag[:name] } + .map do |name, tags| + tags.first.merge(score: tags.sum { |t| t[:score] }) + end # Aggregate scores per tag + .sort_by { |tag| -tag[:score] } .take(5) end From 33513060087d90b9f01e0cc2291b854d8955c319 Mon Sep 17 00:00:00 2001 From: Keegan George Date: Tue, 26 Nov 2024 15:20:40 -0800 Subject: [PATCH 2/8] DEV: Updates --- .../components/ai-split-topic-suggester.gjs | 20 +- .../components/ai-suggestion-dropdown.gjs | 251 ------------------ .../ai-category-suggester.gjs | 21 +- .../suggestion-menus/ai-tag-suggester.gjs | 197 ++++++++++++++ .../suggestion-menus/ai-title-suggester.gjs | 122 +++++++++ .../ai-tag-suggestion.gjs | 197 +------------- .../ai-title-suggestion.gjs | 8 +- .../discourse/lib/ai-helper-suggestions.js | 1 + .../modules/ai-helper/common/ai-helper.scss | 8 +- .../ai_helper/ai_composer_helper_spec.rb | 2 +- .../components/ai_suggestion_dropdown.rb | 2 +- 11 files changed, 355 insertions(+), 474 deletions(-) delete mode 100644 assets/javascripts/discourse/components/ai-suggestion-dropdown.gjs create mode 100644 assets/javascripts/discourse/components/suggestion-menus/ai-tag-suggester.gjs create mode 100644 assets/javascripts/discourse/components/suggestion-menus/ai-title-suggester.gjs create mode 100644 assets/javascripts/discourse/lib/ai-helper-suggestions.js diff --git a/assets/javascripts/discourse/components/ai-split-topic-suggester.gjs b/assets/javascripts/discourse/components/ai-split-topic-suggester.gjs index df8a38434..2e12c595f 100644 --- a/assets/javascripts/discourse/components/ai-split-topic-suggester.gjs +++ b/assets/javascripts/discourse/components/ai-split-topic-suggester.gjs @@ -54,8 +54,13 @@ export default class AiSplitTopicSuggester extends Component { suggestions.includes(item.name.toLowerCase()) ); this.suggestions = suggestedCategories; - } else { - this.suggestions = result.assistant.map((s) => s.name); + } else if (this.args.mode === this.SUGGESTION_TYPES.tag) { + this.suggestions = result.assistant.map((s) => { + return { + name: s.name, + count: s.count, + }; + }); } }) .catch(popupAjaxError) @@ -132,6 +137,17 @@ export default class AiSplitTopicSuggester extends Component { {{on "click" (fn this.applySuggestion suggestion menu)}} > {{categoryBadge suggestion}} + x + {{suggestion.totalTopicCount}} + + {{else if (eq @mode "suggest_tags")}} +
  • + + x{{suggestion.count}} +
  • {{else}}
  • diff --git a/assets/javascripts/discourse/components/ai-suggestion-dropdown.gjs b/assets/javascripts/discourse/components/ai-suggestion-dropdown.gjs deleted file mode 100644 index 363efc88e..000000000 --- a/assets/javascripts/discourse/components/ai-suggestion-dropdown.gjs +++ /dev/null @@ -1,251 +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) => { - console.log(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); - // console.log("suggest", suggestions); - - 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); - } - - -} diff --git a/assets/javascripts/discourse/components/suggestion-menus/ai-category-suggester.gjs b/assets/javascripts/discourse/components/suggestion-menus/ai-category-suggester.gjs index 5b04097df..ca3d01e4c 100644 --- a/assets/javascripts/discourse/components/suggestion-menus/ai-category-suggester.gjs +++ b/assets/javascripts/discourse/components/suggestion-menus/ai-category-suggester.gjs @@ -3,7 +3,7 @@ import { tracked } from "@glimmer/tracking"; import { fn } from "@ember/helper"; import { on } from "@ember/modifier"; import { action } from "@ember/object"; -import { inject as service } from "@ember/service"; +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"; @@ -11,6 +11,7 @@ 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; @@ -19,23 +20,9 @@ export default class AiCategorySuggester extends Component { @tracked untriggers = []; @tracked triggerIcon = "discourse-sparkles"; - get referenceText() { - if (this.args.composer?.reply) { - return this.args.composer.reply; - } - - console.log(this.args); - ajax(`/raw/${this.args.topic.id}/1.json`).then((response) => { - console.log(response); - }); - - return "abcdefhg"; - } - get showSuggestionButton() { - const MIN_CHARACTER_COUNT = 40; const composerFields = document.querySelector(".composer-fields"); - const showTrigger = this.referenceText.length > MIN_CHARACTER_COUNT; + const showTrigger = this.args.composer.reply?.length > MIN_CHARACTER_COUNT; if (composerFields) { if (showTrigger) { @@ -127,7 +114,7 @@ export default class AiCategorySuggester extends Component { >
    {{categoryBadge suggestion}} - x + x {{suggestion.topicCount}}
    diff --git a/assets/javascripts/discourse/components/suggestion-menus/ai-tag-suggester.gjs b/assets/javascripts/discourse/components/suggestion-menus/ai-tag-suggester.gjs new file mode 100644 index 000000000..c29aa1997 --- /dev/null +++ b/assets/javascripts/discourse/components/suggestion-menus/ai-tag-suggester.gjs @@ -0,0 +1,197 @@ +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"; + + get showSuggestionButton() { + const composerFields = document.querySelector(".composer-fields"); + const showTrigger = this.args.composer.reply?.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.args.composer.reply }, + }); + this.suggestions = assistant; + + if (this.#tagSelectorHasValues()) { + this.suggestions = this.suggestions.filter( + (s) => !this.args.composer.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 composer = this.args.composer; + if (!composer) { + return; + } + + if (!composer.tags) { + composer.set("tags", [suggestion.name]); + this.#removedAppliedTag(suggestion); + return; + } + + const tags = composer.tags; + + 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); + composer.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"; + } + } + + +} diff --git a/assets/javascripts/discourse/components/suggestion-menus/ai-title-suggester.gjs b/assets/javascripts/discourse/components/suggestion-menus/ai-title-suggester.gjs new file mode 100644 index 000000000..f7634493d --- /dev/null +++ b/assets/javascripts/discourse/components/suggestion-menus/ai-title-suggester.gjs @@ -0,0 +1,122 @@ +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 { 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 { + @service siteSettings; + @tracked loading = false; + @tracked suggestions = null; + @tracked untriggers = []; + @tracked triggerIcon = "discourse-sparkles"; + + get showSuggestionButton() { + const composerFields = document.querySelector(".composer-fields"); + const showTrigger = this.args.composer.reply?.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 { suggestions } = await ajax( + "/discourse-ai/ai-helper/suggest_title", + { + method: "POST", + data: { text: this.args.composer.reply }, + } + ); + this.suggestions = suggestions; + } catch (error) { + popupAjaxError(error); + } finally { + this.loading = false; + this.triggerIcon = "sync-alt"; + } + + return this.suggestions; + } + + @action + applySuggestion(suggestion) { + const composer = this.args.composer; + if (!composer) { + return; + } + + composer.set("title", suggestion); + this.dMenu.close(); + } + + @action + onRegisterApi(api) { + this.dMenu = api; + } + + @action + onClose() { + this.triggerIcon = "discourse-sparkles"; + } + + +} diff --git a/assets/javascripts/discourse/connectors/after-composer-tag-input/ai-tag-suggestion.gjs b/assets/javascripts/discourse/connectors/after-composer-tag-input/ai-tag-suggestion.gjs index c31af90eb..ac6ad686e 100644 --- a/assets/javascripts/discourse/connectors/after-composer-tag-input/ai-tag-suggestion.gjs +++ b/assets/javascripts/discourse/connectors/after-composer-tag-input/ai-tag-suggestion.gjs @@ -1,16 +1,5 @@ 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 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 { service } from "@ember/service"; +import AiTagSuggester from "../../components/suggestion-menus/ai-tag-suggester"; import { showComposerAiHelper } from "../../lib/show-ai-helper"; export default class AiTagSuggestion extends Component { @@ -23,189 +12,7 @@ export default class AiTagSuggestion extends Component { ); } - @service siteSettings; - @service toasts; - @tracked loading = false; - @tracked suggestions = null; - @tracked untriggers = []; - @tracked triggerIcon = "discourse-sparkles"; - - get showSuggestionButton() { - const MIN_CHARACTER_COUNT = 40; - const composerFields = document.querySelector(".composer-fields"); - const showTrigger = - this.args.outletArgs.composer.reply?.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.args.outletArgs.composer.reply }, - }); - this.suggestions = assistant; - - if (this.#tagSelectorHasValues()) { - this.suggestions = this.suggestions.filter( - (s) => !this.args.outletArgs.composer.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.outletArgs.composer?.tags && - this.args.outletArgs.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 composer = this.args.outletArgs.composer; - if (!composer) { - return; - } - - if (!composer.tags) { - composer.set("tags", [suggestion.name]); - this.#removedAppliedTag(suggestion); - return; - } - - const tags = composer.tags; - - 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); - composer.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"; - } - } - } diff --git a/assets/javascripts/discourse/connectors/after-composer-title-input/ai-title-suggestion.gjs b/assets/javascripts/discourse/connectors/after-composer-title-input/ai-title-suggestion.gjs index 3377c0860..691f210dd 100644 --- a/assets/javascripts/discourse/connectors/after-composer-title-input/ai-title-suggestion.gjs +++ b/assets/javascripts/discourse/connectors/after-composer-title-input/ai-title-suggestion.gjs @@ -1,5 +1,5 @@ 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"; export default class AiTitleSuggestion extends Component { @@ -13,10 +13,6 @@ export default class AiTitleSuggestion extends Component { } } diff --git a/assets/javascripts/discourse/lib/ai-helper-suggestions.js b/assets/javascripts/discourse/lib/ai-helper-suggestions.js new file mode 100644 index 000000000..91ec2e946 --- /dev/null +++ b/assets/javascripts/discourse/lib/ai-helper-suggestions.js @@ -0,0 +1 @@ +export const MIN_CHARACTER_COUNT = 40; diff --git a/assets/stylesheets/modules/ai-helper/common/ai-helper.scss b/assets/stylesheets/modules/ai-helper/common/ai-helper.scss index f01d54881..4867215c5 100644 --- a/assets/stylesheets/modules/ai-helper/common/ai-helper.scss +++ b/assets/stylesheets/modules/ai-helper/common/ai-helper.scss @@ -229,7 +229,8 @@ } .ai-category-suggester-content, -.ai-tag-suggester-content { +.ai-tag-suggester-content, +.ai-title-suggester-content { z-index: z("composer", "dropdown"); } @@ -400,6 +401,11 @@ cursor: pointer; } } + + .topic-count { + font-size: var(--font-down-2); + color: var(--primary-high); + } } .fk-d-menu[data-identifier="ai-split-topic-suggestion-menu"] { diff --git a/spec/system/ai_helper/ai_composer_helper_spec.rb b/spec/system/ai_helper/ai_composer_helper_spec.rb index 5f76386dc..be18c07e9 100644 --- a/spec/system/ai_helper/ai_composer_helper_spec.rb +++ b/spec/system/ai_helper/ai_composer_helper_spec.rb @@ -15,7 +15,7 @@ let(:composer) { PageObjects::Components::Composer.new } let(:ai_helper_menu) { PageObjects::Components::AiComposerHelperMenu.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 } fab!(:category) diff --git a/spec/system/page_objects/components/ai_suggestion_dropdown.rb b/spec/system/page_objects/components/ai_suggestion_dropdown.rb index bb7a910c9..3eac57f3d 100644 --- a/spec/system/page_objects/components/ai_suggestion_dropdown.rb +++ b/spec/system/page_objects/components/ai_suggestion_dropdown.rb @@ -2,7 +2,7 @@ module PageObjects module Components - class AISuggestionDropdown < PageObjects::Components::Base + class AiSuggestionDropdown < PageObjects::Components::Base SUGGESTION_BUTTON_SELECTOR = ".suggestion-button" TITLE_BUTTON_SELECTOR = "#{SUGGESTION_BUTTON_SELECTOR}.suggest-titles-button" CATEGORY_BUTTON_SELECTOR = "#{SUGGESTION_BUTTON_SELECTOR}.suggest-category-button" From aaa280b423db8c2dd10d9739b24d548305bdd67a Mon Sep 17 00:00:00 2001 From: Keegan George Date: Tue, 26 Nov 2024 15:23:10 -0800 Subject: [PATCH 3/8] remove --- .../edit-topic-category__after/ai-category-suggestion.gjs | 1 - 1 file changed, 1 deletion(-) diff --git a/assets/javascripts/discourse/connectors/edit-topic-category__after/ai-category-suggestion.gjs b/assets/javascripts/discourse/connectors/edit-topic-category__after/ai-category-suggestion.gjs index 379345b0d..9efacd7e3 100644 --- a/assets/javascripts/discourse/connectors/edit-topic-category__after/ai-category-suggestion.gjs +++ b/assets/javascripts/discourse/connectors/edit-topic-category__after/ai-category-suggestion.gjs @@ -13,7 +13,6 @@ export default class AiCategorySuggestion extends Component { }