diff --git a/assets/javascripts/discourse/connectors/full-page-search-below-search-header/semantic-search.gjs b/assets/javascripts/discourse/connectors/full-page-search-below-search-header/semantic-search.gjs index bb1da3f8..1e3d84a3 100644 --- a/assets/javascripts/discourse/connectors/full-page-search-below-search-header/semantic-search.gjs +++ b/assets/javascripts/discourse/connectors/full-page-search-below-search-header/semantic-search.gjs @@ -2,15 +2,15 @@ import Component from "@glimmer/component"; import { tracked } from "@glimmer/tracking"; import { on } from "@ember/modifier"; import { action } from "@ember/object"; -import didInsert from "@ember/render-modifiers/modifiers/did-insert"; +import didUpdate from "@ember/render-modifiers/modifiers/did-update"; import { service } from "@ember/service"; import DToggleSwitch from "discourse/components/d-toggle-switch"; import { SEARCH_TYPE_DEFAULT } from "discourse/controllers/full-page-search"; import { ajax } from "discourse/lib/ajax"; -import { withPluginApi } from "discourse/lib/plugin-api"; import { isValidSearchTerm, translateResults } from "discourse/lib/search"; import icon from "discourse-common/helpers/d-icon"; -import I18n from "I18n"; +import I18n, { i18n } from "discourse-i18n"; +import DTooltip from "float-kit/components/d-tooltip"; import AiIndicatorWave from "../../components/ai-indicator-wave"; export default class SemanticSearch extends Component { @@ -18,44 +18,59 @@ export default class SemanticSearch extends Component { return siteSettings.ai_embeddings_semantic_search_enabled; } - @service router; @service appEvents; + @service router; @service siteSettings; @service searchPreferencesManager; - @tracked searching = false; + @tracked searching; @tracked AiResults = []; @tracked showingAiResults = false; + @tracked sortOrder = this.args.outletArgs.sortOrder; initialSearchTerm = this.args.outletArgs.search; + constructor() { + super(...arguments); + this.appEvents.on("full-page-search:trigger-search", this, this.onSearch); + this.onSearch(); + } + + willDestroy() { + super.willDestroy(...arguments); + this.appEvents.off("full-page-search:trigger-search", this, this.onSearch); + } + + @action + onSearch() { + if (!this.searchEnabled) { + return; + } + + this.initialSearchTerm = this.args.outletArgs.search; + this.searching = true; + this.resetAiResults(); + return this.performHyDESearch(); + } + get disableToggleSwitch() { if ( this.searching || this.AiResults.length === 0 || - this.args.outletArgs.sortOrder !== 0 + !this.validSearchOrder ) { return true; } } + get validSearchOrder() { + return this.sortOrder === 0; + } + get searchStateText() { - // Search results: - if (this.AiResults.length > 0) { - if (this.showingAiResults) { - return I18n.t( - "discourse_ai.embeddings.semantic_search_results.toggle", - { - count: this.AiResults.length, - } - ); - } else { - return I18n.t( - "discourse_ai.embeddings.semantic_search_results.toggle_hidden", - { - count: this.AiResults.length, - } - ); - } + if (!this.validSearchOrder) { + return I18n.t( + "discourse_ai.embeddings.semantic_search_results.unavailable" + ); } // Search loading: @@ -63,6 +78,23 @@ export default class SemanticSearch extends Component { return I18n.t("discourse_ai.embeddings.semantic_search_loading"); } + // We have results and we are showing them + if (this.AiResults.length && this.showingAiResults) { + return I18n.t("discourse_ai.embeddings.semantic_search_results.toggle", { + count: this.AiResults.length, + }); + } + + // We have results but are hiding them + if (this.AiResults.length && !this.showingAiResults) { + return I18n.t( + "discourse_ai.embeddings.semantic_search_results.toggle_hidden", + { + count: this.AiResults.length, + } + ); + } + // Typing to search: if ( this.AiResults.length === 0 && @@ -89,7 +121,7 @@ export default class SemanticSearch extends Component { return ( this.args.outletArgs.type === SEARCH_TYPE_DEFAULT && isValidSearchTerm(this.searchTerm, this.siteSettings) && - this.args.outletArgs.sortOrder === 0 + this.validSearchOrder ); } @@ -110,21 +142,7 @@ export default class SemanticSearch extends Component { this.args.outletArgs.addSearchResults([], "topic_id"); } - @action - handleSearch() { - if (!this.searchEnabled) { - return; - } - - if (this.initialSearchTerm && !this.searching) { - return this.performHyDESearch(); - } - - this.#resetAndSearchOnEvent(); - } - performHyDESearch() { - this.searching = true; this.resetAiResults(); ajax("/discourse-ai/embeddings/semantic-search", { @@ -134,7 +152,6 @@ export default class SemanticSearch extends Component { const model = (await translateResults(results)) || {}; if (model.posts?.length === 0) { - this.searching = false; return; } @@ -144,56 +161,68 @@ export default class SemanticSearch extends Component { this.AiResults = model.posts; }) - .finally(() => (this.searching = false)); - } - - #resetAndSearchOnEvent() { - return withPluginApi("1.15.0", (api) => { - api.onAppEvent("full-page-search:trigger-search", () => { - if (!this.searching) { - this.resetAiResults(); - return this.performHyDESearch(); - } + .finally(() => { + this.searching = false; }); - }); } @action - checkQueryParamsAndSearch() { - // This check is necessary because handleSearch() isn't called - // if query params are present and a new search has appended text. - // It ensures AiResults are reset and searched for properly - const searchQueryParam = this.router.currentRoute?.queryParams?.q; - if (searchQueryParam) { - this.#resetAndSearchOnEvent(); + sortChanged() { + if (this.sortOrder !== this.args.outletArgs.sortOrder) { + this.sortOrder = this.args.outletArgs.sortOrder; + + if (this.validSearchOrder) { + this.onSearch(); + } else { + this.showingAiResults = false; + this.resetAiResults(); + } } } } diff --git a/assets/stylesheets/modules/embeddings/common/semantic-search.scss b/assets/stylesheets/modules/embeddings/common/semantic-search.scss index c6e67aa9..673c0785 100644 --- a/assets/stylesheets/modules/embeddings/common/semantic-search.scss +++ b/assets/stylesheets/modules/embeddings/common/semantic-search.scss @@ -16,7 +16,8 @@ display: flex; align-items: center; - &.in-progress { + &.in-progress, + &.unavailable { .semantic-search__searching-text { color: var(--primary-medium); } @@ -25,7 +26,11 @@ &__searching-text { display: inline-block; - margin-left: 3px; + margin-left: 8px; + } + + &__unavailable-tooltip { + margin-left: 4px; } } diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index f34d1d17..14c9db6b 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -483,6 +483,8 @@ en: toggle_hidden: "Hiding %{count} results found using AI" none: "Sorry, our AI search found no matching topics" new: "Press 'search' to begin looking for new results with AI" + unavailable: "AI results unavailable" + semantic_search_unavailable_tooltip: "Search results must be sorted by Relevance to display AI results" ai_generated_result: "Search result found using AI" quick_search: suffix: "in all topics and posts with AI" diff --git a/lib/embeddings/entry_point.rb b/lib/embeddings/entry_point.rb index 47df06fc..66047af2 100644 --- a/lib/embeddings/entry_point.rb +++ b/lib/embeddings/entry_point.rb @@ -4,6 +4,9 @@ module DiscourseAi module Embeddings class EntryPoint def inject_into(plugin) + # far-circle-question used by semantic search unavailable tooltip + plugin.register_svg_icon "far-circle-question" if plugin.respond_to?(:register_svg_icon) + # Include random topics in the suggested list *only* if there are no related topics. plugin.register_modifier( :topic_view_suggested_topics_options,