Skip to content
This repository was archived by the owner on Jul 22, 2025. It is now read-only.

Commit 69d1486

Browse files
committed
WIP
1 parent f75b13c commit 69d1486

File tree

8 files changed

+427
-47
lines changed

8 files changed

+427
-47
lines changed

assets/javascripts/discourse/components/ai-suggestion-dropdown.gjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ export default class AISuggestionDropdown extends Component {
131131
data: { text: this.composer.model.reply },
132132
})
133133
.then((data) => {
134+
console.log(data);
134135
this.#assignGeneratedSuggestions(data, this.args.mode);
135136
})
136137
.catch(popupAjaxError)
@@ -198,6 +199,7 @@ export default class AISuggestionDropdown extends Component {
198199
}
199200

200201
const suggestions = data.assistant.map((s) => s.name);
202+
// console.log("suggest", suggestions);
201203

202204
if (mode === this.SUGGESTION_TYPES.tag) {
203205
if (this.#tagSelectorHasValues()) {
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import Component from "@glimmer/component";
2+
import { tracked } from "@glimmer/tracking";
3+
import { fn } from "@ember/helper";
4+
import { on } from "@ember/modifier";
5+
import { action } from "@ember/object";
6+
import { inject as service } from "@ember/service";
7+
import DButton from "discourse/components/d-button";
8+
import DropdownMenu from "discourse/components/dropdown-menu";
9+
import categoryBadge from "discourse/helpers/category-badge";
10+
import { ajax } from "discourse/lib/ajax";
11+
import { popupAjaxError } from "discourse/lib/ajax-error";
12+
import i18n from "discourse-common/helpers/i18n";
13+
import DMenu from "float-kit/components/d-menu";
14+
15+
export default class AiCategorySuggester extends Component {
16+
@service siteSettings;
17+
@tracked loading = false;
18+
@tracked suggestions = null;
19+
@tracked untriggers = [];
20+
@tracked triggerIcon = "discourse-sparkles";
21+
22+
get referenceText() {
23+
if (this.args.composer?.reply) {
24+
return this.args.composer.reply;
25+
}
26+
27+
console.log(this.args);
28+
ajax(`/raw/${this.args.topic.id}/1.json`).then((response) => {
29+
console.log(response);
30+
});
31+
32+
return "abcdefhg";
33+
}
34+
35+
get showSuggestionButton() {
36+
const MIN_CHARACTER_COUNT = 40;
37+
const composerFields = document.querySelector(".composer-fields");
38+
const showTrigger = this.referenceText.length > MIN_CHARACTER_COUNT;
39+
40+
if (composerFields) {
41+
if (showTrigger) {
42+
composerFields.classList.add("showing-ai-suggestions");
43+
} else {
44+
composerFields.classList.remove("showing-ai-suggestions");
45+
}
46+
}
47+
48+
return this.siteSettings.ai_embeddings_enabled && showTrigger;
49+
}
50+
51+
@action
52+
async loadSuggestions() {
53+
if (this.suggestions && !this.dMenu.expanded) {
54+
return this.suggestions;
55+
}
56+
57+
this.loading = true;
58+
this.triggerIcon = "spinner";
59+
60+
try {
61+
const { assistant } = await ajax(
62+
"/discourse-ai/ai-helper/suggest_category",
63+
{
64+
method: "POST",
65+
data: { text: this.args.composer.reply },
66+
}
67+
);
68+
this.suggestions = assistant;
69+
} catch (error) {
70+
popupAjaxError(error);
71+
} finally {
72+
this.loading = false;
73+
this.triggerIcon = "sync-alt";
74+
}
75+
76+
return this.suggestions;
77+
}
78+
79+
@action
80+
applySuggestion(suggestion) {
81+
const composer = this.args.composer;
82+
if (!composer) {
83+
return;
84+
}
85+
86+
composer.set("categoryId", suggestion.id);
87+
this.dMenu.close();
88+
}
89+
90+
@action
91+
onRegisterApi(api) {
92+
this.dMenu = api;
93+
}
94+
95+
@action
96+
onClose() {
97+
this.triggerIcon = "discourse-sparkles";
98+
}
99+
100+
<template>
101+
{{#if this.showSuggestionButton}}
102+
<DMenu
103+
@title={{i18n "discourse_ai.ai_helper.suggest"}}
104+
@icon={{this.triggerIcon}}
105+
@identifier="ai-category-suggester"
106+
@onClose={{this.onClose}}
107+
@triggerClass="suggestion-button suggest-category-button {{if
108+
this.loading
109+
'is-loading'
110+
}}"
111+
@onRegisterApi={{this.onRegisterApi}}
112+
@modalForMobile={{true}}
113+
@untriggers={{this.untriggers}}
114+
{{on "click" this.loadSuggestions}}
115+
>
116+
<:content>
117+
{{#unless this.loading}}
118+
<DropdownMenu as |dropdown|>
119+
{{#each this.suggestions as |suggestion|}}
120+
<dropdown.item>
121+
<DButton
122+
class="category-row"
123+
data-title={{suggestion.name}}
124+
data-value={{suggestion.id}}
125+
title={{suggestion.name}}
126+
@action={{fn this.applySuggestion suggestion}}
127+
>
128+
<div class="category-status">
129+
{{categoryBadge suggestion}}
130+
<span class="topic-count" aria-label="">x
131+
{{suggestion.topicCount}}</span>
132+
</div>
133+
</DButton>
134+
</dropdown.item>
135+
{{/each}}
136+
</DropdownMenu>
137+
{{/unless}}
138+
</:content>
139+
</DMenu>
140+
{{/if}}
141+
</template>
142+
}
Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import Component from "@glimmer/component";
2-
import { inject as service } from "@ember/service";
3-
import AISuggestionDropdown from "../../components/ai-suggestion-dropdown";
2+
import AiCategorySuggester from "../../components/suggestion-menus/ai-category-suggester";
43
import { showComposerAiHelper } from "../../lib/show-ai-helper";
54

65
export default class AiCategorySuggestion extends Component {
@@ -13,15 +12,7 @@ export default class AiCategorySuggestion extends Component {
1312
);
1413
}
1514

16-
@service siteSettings;
17-
1815
<template>
19-
{{#if this.siteSettings.ai_embeddings_enabled}}
20-
<AISuggestionDropdown
21-
@mode="suggest_category"
22-
@composer={{@outletArgs.composer}}
23-
class="suggest-category-button"
24-
/>
25-
{{/if}}
16+
<AiCategorySuggester @composer={{@outletArgs.composer}} />
2617
</template>
2718
}
Lines changed: 191 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
import Component from "@glimmer/component";
2+
import { tracked } from "@glimmer/tracking";
3+
import { fn } from "@ember/helper";
4+
import { on } from "@ember/modifier";
5+
import { action } from "@ember/object";
26
import { inject as service } from "@ember/service";
3-
import AISuggestionDropdown from "../../components/ai-suggestion-dropdown";
7+
import DButton from "discourse/components/d-button";
8+
import DropdownMenu from "discourse/components/dropdown-menu";
9+
import discourseTag from "discourse/helpers/discourse-tag";
10+
import { ajax } from "discourse/lib/ajax";
11+
import { popupAjaxError } from "discourse/lib/ajax-error";
12+
import i18n from "discourse-common/helpers/i18n";
13+
import DMenu from "float-kit/components/d-menu";
414
import { showComposerAiHelper } from "../../lib/show-ai-helper";
515

616
export default class AiTagSuggestion extends Component {
@@ -14,14 +24,188 @@ export default class AiTagSuggestion extends Component {
1424
}
1525

1626
@service siteSettings;
27+
@service toasts;
28+
@tracked loading = false;
29+
@tracked suggestions = null;
30+
@tracked untriggers = [];
31+
@tracked triggerIcon = "discourse-sparkles";
32+
33+
get showSuggestionButton() {
34+
const MIN_CHARACTER_COUNT = 40;
35+
const composerFields = document.querySelector(".composer-fields");
36+
const showTrigger =
37+
this.args.outletArgs.composer.reply?.length > MIN_CHARACTER_COUNT;
38+
39+
if (composerFields) {
40+
if (showTrigger) {
41+
composerFields.classList.add("showing-ai-suggestions");
42+
} else {
43+
composerFields.classList.remove("showing-ai-suggestions");
44+
}
45+
}
46+
47+
return this.siteSettings.ai_embeddings_enabled && showTrigger;
48+
}
49+
50+
get showDropdown() {
51+
if (this.suggestions?.length <= 0) {
52+
this.dMenu.close();
53+
}
54+
return !this.loading && this.suggestions?.length > 0;
55+
}
56+
57+
@action
58+
async loadSuggestions() {
59+
if (
60+
this.suggestions &&
61+
this.suggestions?.length > 0 &&
62+
!this.dMenu.expanded
63+
) {
64+
return this.suggestions;
65+
}
66+
67+
this.loading = true;
68+
this.triggerIcon = "spinner";
69+
70+
try {
71+
const { assistant } = await ajax("/discourse-ai/ai-helper/suggest_tags", {
72+
method: "POST",
73+
data: { text: this.args.outletArgs.composer.reply },
74+
});
75+
this.suggestions = assistant;
76+
77+
if (this.#tagSelectorHasValues()) {
78+
this.suggestions = this.suggestions.filter(
79+
(s) => !this.args.outletArgs.composer.tags.includes(s.name)
80+
);
81+
}
82+
83+
if (this.suggestions?.length <= 0) {
84+
this.toasts.error({
85+
class: "ai-suggestion-error",
86+
duration: 3000,
87+
data: {
88+
message: i18n(
89+
"discourse_ai.ai_helper.suggest_errors.no_suggestions"
90+
),
91+
},
92+
});
93+
return;
94+
}
95+
} catch (error) {
96+
popupAjaxError(error);
97+
} finally {
98+
this.loading = false;
99+
this.triggerIcon = "sync-alt";
100+
}
101+
102+
return this.suggestions;
103+
}
104+
105+
#tagSelectorHasValues() {
106+
return (
107+
this.args.outletArgs.composer?.tags &&
108+
this.args.outletArgs.composer?.tags.length > 0
109+
);
110+
}
111+
112+
#removedAppliedTag(suggestion) {
113+
return (this.suggestions = this.suggestions.filter(
114+
(s) => s.id !== suggestion.id
115+
));
116+
}
117+
118+
@action
119+
applySuggestion(suggestion) {
120+
const maxTags = this.siteSettings.max_tags_per_topic;
121+
const composer = this.args.outletArgs.composer;
122+
if (!composer) {
123+
return;
124+
}
125+
126+
if (!composer.tags) {
127+
composer.set("tags", [suggestion.name]);
128+
this.#removedAppliedTag(suggestion);
129+
return;
130+
}
131+
132+
const tags = composer.tags;
133+
134+
if (tags?.length >= maxTags) {
135+
return this.toasts.error({
136+
class: "ai-suggestion-error",
137+
duration: 3000,
138+
data: {
139+
message: i18n("discourse_ai.ai_helper.suggest_errors.too_many_tags", {
140+
count: maxTags,
141+
}),
142+
},
143+
});
144+
}
145+
146+
tags.push(suggestion.name);
147+
composer.set("tags", [...tags]);
148+
suggestion.disabled = true;
149+
this.#removedAppliedTag(suggestion);
150+
}
151+
152+
@action
153+
onRegisterApi(api) {
154+
this.dMenu = api;
155+
}
156+
157+
@action
158+
onClose() {
159+
if (this.suggestions?.length > 0) {
160+
// If all suggestions have been used,
161+
// re-triggering when no suggestions present
162+
// will cause computation issues with
163+
// setting the icon, so we prevent it
164+
this.triggerIcon = "discourse-sparkles";
165+
}
166+
}
17167

18168
<template>
19-
{{#if this.siteSettings.ai_embeddings_enabled}}
20-
<AISuggestionDropdown
21-
@mode="suggest_tags"
22-
@composer={{@outletArgs.composer}}
23-
class="suggest-tags-button"
24-
/>
169+
{{#if this.showSuggestionButton}}
170+
<DMenu
171+
@title={{i18n "discourse_ai.ai_helper.suggest"}}
172+
@icon={{this.triggerIcon}}
173+
@identifier="ai-tag-suggester"
174+
@onClose={{this.onClose}}
175+
@triggerClass="suggestion-button suggest-tags-button {{if
176+
this.loading
177+
'is-loading'
178+
}}"
179+
@onRegisterApi={{this.onRegisterApi}}
180+
@modalForMobile={{true}}
181+
@untriggers={{this.untriggers}}
182+
{{on "click" this.loadSuggestions}}
183+
>
184+
<:content>
185+
{{#if this.showDropdown}}
186+
<DropdownMenu as |dropdown|>
187+
{{#each this.suggestions as |suggestion|}}
188+
<dropdown.item>
189+
<DButton
190+
class="tag-row"
191+
data-title={{suggestion.name}}
192+
data-value={{suggestion.id}}
193+
title={{suggestion.name}}
194+
@disabled={{this.isDisabled suggestion}}
195+
@action={{fn this.applySuggestion suggestion}}
196+
>
197+
{{discourseTag
198+
suggestion.name
199+
count=suggestion.count
200+
noHref=true
201+
}}
202+
</DButton>
203+
</dropdown.item>
204+
{{/each}}
205+
</DropdownMenu>
206+
{{/if}}
207+
</:content>
208+
</DMenu>
25209
{{/if}}
26210
</template>
27211
}

0 commit comments

Comments
 (0)