FEATURE: Unavailable state for semantic search when sort is not Relevant (#1030)
This commit adds an "unavailable" state for the AI semantic search toggle. Currently the AI toggle disappears when the sort by is anything but Relevance which makes the UI confusing for users looking for AI results. This should help!
This commit is contained in:
parent
534b0df391
commit
24b107881a
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
<span {{didInsert this.checkQueryParamsAndSearch}}></span>
|
||||
{{#if this.searchEnabled}}
|
||||
<div class="semantic-search__container search-results" role="region">
|
||||
<div class="semantic-search__results" {{didInsert this.handleSearch}}>
|
||||
<div
|
||||
class="semantic-search__searching
|
||||
{{if this.searching 'in-progress'}}"
|
||||
>
|
||||
<DToggleSwitch
|
||||
disabled={{this.disableToggleSwitch}}
|
||||
@state={{this.showingAiResults}}
|
||||
class="semantic-search__results-toggle"
|
||||
{{on "click" this.toggleAiResults}}
|
||||
/>
|
||||
<span {{didUpdate this.sortChanged @outletArgs.sortOrder}}></span>
|
||||
<div class="semantic-search__container search-results" role="region">
|
||||
<div class="semantic-search__results">
|
||||
<div
|
||||
class="semantic-search__searching
|
||||
{{if this.searching 'in-progress'}}
|
||||
{{unless this.validSearchOrder 'unavailable'}}"
|
||||
>
|
||||
<DToggleSwitch
|
||||
disabled={{this.disableToggleSwitch}}
|
||||
@state={{this.showingAiResults}}
|
||||
class="semantic-search__results-toggle"
|
||||
{{on "click" this.toggleAiResults}}
|
||||
/>
|
||||
|
||||
<div class="semantic-search__searching-text">
|
||||
{{icon "discourse-sparkles"}}
|
||||
{{this.searchStateText}}
|
||||
</div>
|
||||
|
||||
<AiIndicatorWave @loading={{this.searching}} />
|
||||
<div class="semantic-search__searching-text">
|
||||
{{icon "discourse-sparkles"}}
|
||||
{{this.searchStateText}}
|
||||
</div>
|
||||
|
||||
{{#if this.validSearchOrder}}
|
||||
<AiIndicatorWave @loading={{this.searching}} />
|
||||
{{/if}}
|
||||
|
||||
{{#unless this.validSearchOrder}}
|
||||
|
||||
<DTooltip
|
||||
@identifier="semantic-search-unavailable-tooltip"
|
||||
class="semantic-search__unavailable-tooltip"
|
||||
>
|
||||
<:trigger>
|
||||
{{icon "far-circle-question"}}
|
||||
</:trigger>
|
||||
<:content>
|
||||
{{i18n
|
||||
"discourse_ai.embeddings.semantic_search_unavailable_tooltip"
|
||||
}}
|
||||
</:content>
|
||||
</DTooltip>
|
||||
{{/unless}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</template>
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue