UX: Show error and ability to try again when no suggestions
## 🔍 Overview
When the title suggestions return no suggestions, there is no indication in the UI. In the tag suggester we show a toast when there aren't any suggestions but the request was a success. In this update we make a similar UI indication with a toast for both category and title suggestions. Additionally, for all suggestions we add a "Try again" button to the toast so that suggestions can be generated again if the results yield nothing the first time.
This commit is contained in:
parent
a907bc891a
commit
d777b8a75a
|
@ -11,7 +11,10 @@ import { ajax } from "discourse/lib/ajax";
|
||||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
import { i18n } from "discourse-i18n";
|
import { i18n } from "discourse-i18n";
|
||||||
import DMenu from "float-kit/components/d-menu";
|
import DMenu from "float-kit/components/d-menu";
|
||||||
import { MIN_CHARACTER_COUNT } from "../../lib/ai-helper-suggestions";
|
import {
|
||||||
|
MIN_CHARACTER_COUNT,
|
||||||
|
showSuggestionsError,
|
||||||
|
} from "../../lib/ai-helper-suggestions";
|
||||||
|
|
||||||
export default class AiCategorySuggester extends Component {
|
export default class AiCategorySuggester extends Component {
|
||||||
@service siteSettings;
|
@service siteSettings;
|
||||||
|
@ -40,9 +43,20 @@ export default class AiCategorySuggester extends Component {
|
||||||
return this.siteSettings.ai_embeddings_enabled && showTrigger;
|
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
|
@action
|
||||||
async loadSuggestions() {
|
async loadSuggestions() {
|
||||||
if (this.suggestions && !this.dMenu.expanded) {
|
if (
|
||||||
|
this.suggestions &&
|
||||||
|
this.suggestions?.length > 0 &&
|
||||||
|
!this.dMenu.expanded
|
||||||
|
) {
|
||||||
return this.suggestions;
|
return this.suggestions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,7 +79,13 @@ export default class AiCategorySuggester extends Component {
|
||||||
data,
|
data,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
this.suggestions = assistant;
|
this.suggestions = assistant;
|
||||||
|
|
||||||
|
if (this.suggestions?.length <= 0) {
|
||||||
|
showSuggestionsError(this, this.loadSuggestions.bind(this));
|
||||||
|
return;
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
popupAjaxError(error);
|
popupAjaxError(error);
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -100,7 +120,13 @@ export default class AiCategorySuggester extends Component {
|
||||||
|
|
||||||
@action
|
@action
|
||||||
onClose() {
|
onClose() {
|
||||||
this.triggerIcon = "discourse-sparkles";
|
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>
|
<template>
|
||||||
|
@ -121,7 +147,7 @@ export default class AiCategorySuggester extends Component {
|
||||||
{{on "click" this.loadSuggestions}}
|
{{on "click" this.loadSuggestions}}
|
||||||
>
|
>
|
||||||
<:content>
|
<:content>
|
||||||
{{#unless this.loading}}
|
{{#if this.showDropdown}}
|
||||||
<DropdownMenu as |dropdown|>
|
<DropdownMenu as |dropdown|>
|
||||||
{{#each this.suggestions as |suggestion index|}}
|
{{#each this.suggestions as |suggestion index|}}
|
||||||
<dropdown.item>
|
<dropdown.item>
|
||||||
|
@ -141,7 +167,7 @@ export default class AiCategorySuggester extends Component {
|
||||||
</dropdown.item>
|
</dropdown.item>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
{{/unless}}
|
{{/if}}
|
||||||
</:content>
|
</:content>
|
||||||
</DMenu>
|
</DMenu>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
|
@ -11,7 +11,10 @@ import { ajax } from "discourse/lib/ajax";
|
||||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
import { i18n } from "discourse-i18n";
|
import { i18n } from "discourse-i18n";
|
||||||
import DMenu from "float-kit/components/d-menu";
|
import DMenu from "float-kit/components/d-menu";
|
||||||
import { MIN_CHARACTER_COUNT } from "../../lib/ai-helper-suggestions";
|
import {
|
||||||
|
MIN_CHARACTER_COUNT,
|
||||||
|
showSuggestionsError,
|
||||||
|
} from "../../lib/ai-helper-suggestions";
|
||||||
|
|
||||||
export default class AiTagSuggester extends Component {
|
export default class AiTagSuggester extends Component {
|
||||||
@service siteSettings;
|
@service siteSettings;
|
||||||
|
@ -79,6 +82,7 @@ export default class AiTagSuggester extends Component {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.suggestions = assistant;
|
this.suggestions = assistant;
|
||||||
|
|
||||||
const model = this.args.composer
|
const model = this.args.composer
|
||||||
|
@ -92,15 +96,7 @@ export default class AiTagSuggester extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.suggestions?.length <= 0) {
|
if (this.suggestions?.length <= 0) {
|
||||||
this.toasts.error({
|
showSuggestionsError(this, this.loadSuggestions.bind(this));
|
||||||
class: "ai-suggestion-error",
|
|
||||||
duration: 3000,
|
|
||||||
data: {
|
|
||||||
message: i18n(
|
|
||||||
"discourse_ai.ai_helper.suggest_errors.no_suggestions"
|
|
||||||
),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
@ -9,7 +9,10 @@ import { ajax } from "discourse/lib/ajax";
|
||||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
import { i18n } from "discourse-i18n";
|
import { i18n } from "discourse-i18n";
|
||||||
import DMenu from "float-kit/components/d-menu";
|
import DMenu from "float-kit/components/d-menu";
|
||||||
import { MIN_CHARACTER_COUNT } from "../../lib/ai-helper-suggestions";
|
import {
|
||||||
|
MIN_CHARACTER_COUNT,
|
||||||
|
showSuggestionsError,
|
||||||
|
} from "../../lib/ai-helper-suggestions";
|
||||||
|
|
||||||
export default class AiTitleSuggester extends Component {
|
export default class AiTitleSuggester extends Component {
|
||||||
@tracked loading = false;
|
@tracked loading = false;
|
||||||
|
@ -46,9 +49,20 @@ export default class AiTitleSuggester extends Component {
|
||||||
return showTrigger;
|
return showTrigger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get showDropdown() {
|
||||||
|
if (this.suggestions?.length <= 0) {
|
||||||
|
this.dMenu.close();
|
||||||
|
}
|
||||||
|
return !this.loading && this.suggestions?.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
async loadSuggestions() {
|
async loadSuggestions() {
|
||||||
if (this.suggestions && !this.dMenu.expanded) {
|
if (
|
||||||
|
this.suggestions &&
|
||||||
|
this.suggestions?.length > 0 &&
|
||||||
|
!this.dMenu.expanded
|
||||||
|
) {
|
||||||
return this.suggestions;
|
return this.suggestions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,7 +84,13 @@ export default class AiTitleSuggester extends Component {
|
||||||
data,
|
data,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
this.suggestions = suggestions;
|
this.suggestions = suggestions;
|
||||||
|
|
||||||
|
if (this.suggestions?.length <= 0) {
|
||||||
|
showSuggestionsError(this, this.loadSuggestions.bind(this));
|
||||||
|
return;
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
popupAjaxError(error);
|
popupAjaxError(error);
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -99,7 +119,13 @@ export default class AiTitleSuggester extends Component {
|
||||||
|
|
||||||
@action
|
@action
|
||||||
onClose() {
|
onClose() {
|
||||||
this.triggerIcon = "discourse-sparkles";
|
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>
|
<template>
|
||||||
|
@ -120,7 +146,7 @@ export default class AiTitleSuggester extends Component {
|
||||||
{{on "click" this.loadSuggestions}}
|
{{on "click" this.loadSuggestions}}
|
||||||
>
|
>
|
||||||
<:content>
|
<:content>
|
||||||
{{#unless this.loading}}
|
{{#if this.showDropdown}}
|
||||||
<DropdownMenu as |dropdown|>
|
<DropdownMenu as |dropdown|>
|
||||||
{{#each this.suggestions as |suggestion index|}}
|
{{#each this.suggestions as |suggestion index|}}
|
||||||
<dropdown.item>
|
<dropdown.item>
|
||||||
|
@ -135,7 +161,7 @@ export default class AiTitleSuggester extends Component {
|
||||||
</dropdown.item>
|
</dropdown.item>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
{{/unless}}
|
{{/if}}
|
||||||
</:content>
|
</:content>
|
||||||
</DMenu>
|
</DMenu>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
|
@ -1 +1,34 @@
|
||||||
|
import { getOwner } from "@ember/application";
|
||||||
|
import { later } from "@ember/runloop";
|
||||||
|
import { i18n } from "discourse-i18n";
|
||||||
|
|
||||||
export const MIN_CHARACTER_COUNT = 40;
|
export const MIN_CHARACTER_COUNT = 40;
|
||||||
|
|
||||||
|
export function showSuggestionsError(context, reloadFn) {
|
||||||
|
const toasts = getOwner(context).lookup("service:toasts");
|
||||||
|
|
||||||
|
toasts.error({
|
||||||
|
class: "ai-suggestion-error",
|
||||||
|
duration: "long",
|
||||||
|
showProgressBar: true,
|
||||||
|
data: {
|
||||||
|
message: i18n("discourse_ai.ai_helper.suggest_errors.no_suggestions"),
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: i18n("discourse_ai.ai_helper.context_menu.regen"),
|
||||||
|
icon: "rotate",
|
||||||
|
class: "btn btn-small",
|
||||||
|
action: async (toast) => {
|
||||||
|
toast.close();
|
||||||
|
|
||||||
|
await reloadFn();
|
||||||
|
|
||||||
|
if (context.dMenu?.show && context.suggestions?.length > 0) {
|
||||||
|
later(() => context.dMenu.show(), 50);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -599,7 +599,7 @@ en:
|
||||||
trigger: "Ask AI"
|
trigger: "Ask AI"
|
||||||
loading: "AI is generating"
|
loading: "AI is generating"
|
||||||
cancel: "Cancel"
|
cancel: "Cancel"
|
||||||
regen: "Try Again"
|
regen: "Try again"
|
||||||
confirm: "Confirm"
|
confirm: "Confirm"
|
||||||
discard: "Discard"
|
discard: "Discard"
|
||||||
changes: "Suggested edits"
|
changes: "Suggested edits"
|
||||||
|
|
Loading…
Reference in New Issue