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:
Keegan George 2025-06-11 11:50:18 -07:00
parent a907bc891a
commit d777b8a75a
No known key found for this signature in database
GPG Key ID: 91B40E38537AC000
5 changed files with 102 additions and 21 deletions

View File

@ -11,7 +11,10 @@ import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { i18n } from "discourse-i18n";
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 {
@service siteSettings;
@ -40,9 +43,20 @@ export default class AiCategorySuggester extends Component {
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.dMenu.expanded) {
if (
this.suggestions &&
this.suggestions?.length > 0 &&
!this.dMenu.expanded
) {
return this.suggestions;
}
@ -65,7 +79,13 @@ export default class AiCategorySuggester extends Component {
data,
}
);
this.suggestions = assistant;
if (this.suggestions?.length <= 0) {
showSuggestionsError(this, this.loadSuggestions.bind(this));
return;
}
} catch (error) {
popupAjaxError(error);
} finally {
@ -100,7 +120,13 @@ export default class AiCategorySuggester extends Component {
@action
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>
@ -121,7 +147,7 @@ export default class AiCategorySuggester extends Component {
{{on "click" this.loadSuggestions}}
>
<:content>
{{#unless this.loading}}
{{#if this.showDropdown}}
<DropdownMenu as |dropdown|>
{{#each this.suggestions as |suggestion index|}}
<dropdown.item>
@ -141,7 +167,7 @@ export default class AiCategorySuggester extends Component {
</dropdown.item>
{{/each}}
</DropdownMenu>
{{/unless}}
{{/if}}
</:content>
</DMenu>
{{/if}}

View File

@ -11,7 +11,10 @@ import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { i18n } from "discourse-i18n";
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 {
@service siteSettings;
@ -79,6 +82,7 @@ export default class AiTagSuggester extends Component {
method: "POST",
data,
});
this.suggestions = assistant;
const model = this.args.composer
@ -92,15 +96,7 @@ export default class AiTagSuggester extends Component {
}
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"
),
},
});
showSuggestionsError(this, this.loadSuggestions.bind(this));
return;
}
} catch (error) {

View File

@ -9,7 +9,10 @@ import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { i18n } from "discourse-i18n";
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 {
@tracked loading = false;
@ -46,9 +49,20 @@ export default class AiTitleSuggester extends Component {
return 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.dMenu.expanded) {
if (
this.suggestions &&
this.suggestions?.length > 0 &&
!this.dMenu.expanded
) {
return this.suggestions;
}
@ -70,7 +84,13 @@ export default class AiTitleSuggester extends Component {
data,
}
);
this.suggestions = suggestions;
if (this.suggestions?.length <= 0) {
showSuggestionsError(this, this.loadSuggestions.bind(this));
return;
}
} catch (error) {
popupAjaxError(error);
} finally {
@ -99,7 +119,13 @@ export default class AiTitleSuggester extends Component {
@action
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>
@ -120,7 +146,7 @@ export default class AiTitleSuggester extends Component {
{{on "click" this.loadSuggestions}}
>
<:content>
{{#unless this.loading}}
{{#if this.showDropdown}}
<DropdownMenu as |dropdown|>
{{#each this.suggestions as |suggestion index|}}
<dropdown.item>
@ -135,7 +161,7 @@ export default class AiTitleSuggester extends Component {
</dropdown.item>
{{/each}}
</DropdownMenu>
{{/unless}}
{{/if}}
</:content>
</DMenu>
{{/if}}

View File

@ -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 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);
}
},
},
],
},
});
}

View File

@ -599,7 +599,7 @@ en:
trigger: "Ask AI"
loading: "AI is generating"
cancel: "Cancel"
regen: "Try Again"
regen: "Try again"
confirm: "Confirm"
discard: "Discard"
changes: "Suggested edits"