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

Commit e666266

Browse files
authored
DEV: Make indicator wave a reusable component (#807)
Previously we had some hardcoded markup with scss making a loading indicator wave. This code was being duplicated and used in both semantic search and summarization. We want to add the indicator wave to the AI helper diff modal as well and have the text flashing instead of the loading spinner. To ensure we do not repeat ourselves, in this PR we turn the summary indicator wave into a reusable template only component called: `AiIndicatorWave`. We then apply the usage of that component to semantic search, summarization, and the composer helper modal.
1 parent 1e15594 commit e666266

File tree

9 files changed

+86
-74
lines changed

9 files changed

+86
-74
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
const indicatorDots = [".", ".", "."];
2+
const AiIndicatorWave = <template>
3+
{{#if @loading}}
4+
<span class="ai-indicator-wave">
5+
{{#each indicatorDots as |dot|}}
6+
<span class="ai-indicator-wave__dot">{{dot}}</span>
7+
{{/each}}
8+
</span>
9+
{{/if}}
10+
</template>;
11+
12+
export default AiIndicatorWave;

assets/javascripts/discourse/components/ai-summary-skeleton.gjs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { cancel } from "@ember/runloop";
99
import concatClass from "discourse/helpers/concat-class";
1010
import i18n from "discourse-common/helpers/i18n";
1111
import discourseLater from "discourse-common/lib/later";
12+
import AiIndicatorWave from "./ai-indicator-wave";
1213

1314
class Block {
1415
@tracked show = false;
@@ -118,11 +119,7 @@ export default class AiSummarySkeleton extends Component {
118119
<div class="ai-summary__generating-text">
119120
{{i18n "summary.in_progress"}}
120121
</div>
121-
<span class="ai-summary__indicator-wave">
122-
<span class="ai-summary__indicator-dot">.</span>
123-
<span class="ai-summary__indicator-dot">.</span>
124-
<span class="ai-summary__indicator-dot">.</span>
125-
</span>
122+
<AiIndicatorWave @loading={{true}} />
126123
</span>
127124
</div>
128125
</template>

assets/javascripts/discourse/components/modal/diff-modal.gjs

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ import { tracked } from "@glimmer/tracking";
33
import { action } from "@ember/object";
44
import { inject as service } from "@ember/service";
55
import { htmlSafe } from "@ember/template";
6-
import ConditionalLoadingSpinner from "discourse/components/conditional-loading-spinner";
76
import CookText from "discourse/components/cook-text";
87
import DButton from "discourse/components/d-button";
98
import DModal from "discourse/components/d-modal";
109
import { ajax } from "discourse/lib/ajax";
1110
import { popupAjaxError } from "discourse/lib/ajax-error";
1211
import i18n from "discourse-common/helpers/i18n";
12+
import AiIndicatorWave from "../ai-indicator-wave";
1313

1414
export default class ModalDiffModal extends Component {
1515
@service currentUser;
@@ -65,25 +65,23 @@ export default class ModalDiffModal extends Component {
6565
@closeModal={{@closeModal}}
6666
>
6767
<:body>
68-
<ConditionalLoadingSpinner @condition={{this.loading}}>
69-
{{#if this.loading}}
70-
<div class="composer-ai-helper-modal__loading">
71-
<CookText @rawText={{this.selectedText}} />
72-
</div>
68+
{{#if this.loading}}
69+
<div class="composer-ai-helper-modal__loading">
70+
<CookText @rawText={{@model.selectedText}} />
71+
</div>
72+
{{else}}
73+
{{#if this.diff}}
74+
{{htmlSafe this.diff}}
7375
{{else}}
74-
{{#if this.diff}}
75-
{{htmlSafe this.diff}}
76-
{{else}}
77-
<div class="composer-ai-helper-modal__old-value">
78-
{{@model.selectedText}}
79-
</div>
76+
<div class="composer-ai-helper-modal__old-value">
77+
{{@model.selectedText}}
78+
</div>
8079

81-
<div class="composer-ai-helper-modal__new-value">
82-
{{this.suggestion}}
83-
</div>
84-
{{/if}}
80+
<div class="composer-ai-helper-modal__new-value">
81+
{{this.suggestion}}
82+
</div>
8583
{{/if}}
86-
</ConditionalLoadingSpinner>
84+
{{/if}}
8785

8886
</:body>
8987

@@ -93,7 +91,9 @@ export default class ModalDiffModal extends Component {
9391
class="btn-primary"
9492
@label="discourse_ai.ai_helper.context_menu.loading"
9593
@disabled={{true}}
96-
/>
94+
>
95+
<AiIndicatorWave @loading={{this.loading}} />
96+
</DButton>
9797
{{else}}
9898
<DButton
9999
class="btn-primary confirm"

assets/javascripts/discourse/connectors/full-page-search-below-search-header/semantic-search.gjs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { withPluginApi } from "discourse/lib/plugin-api";
1111
import { isValidSearchTerm, translateResults } from "discourse/lib/search";
1212
import icon from "discourse-common/helpers/d-icon";
1313
import I18n from "I18n";
14+
import AiIndicatorWave from "../../components/ai-indicator-wave";
1415

1516
export default class SemanticSearch extends Component {
1617
static shouldRender(_args, { siteSettings }) {
@@ -173,13 +174,7 @@ export default class SemanticSearch extends Component {
173174
{{this.searchStateText}}
174175
</div>
175176

176-
{{#if this.searching}}
177-
<span class="semantic-search__indicator-wave">
178-
<span class="semantic-search__indicator-dot">.</span>
179-
<span class="semantic-search__indicator-dot">.</span>
180-
<span class="semantic-search__indicator-dot">.</span>
181-
</span>
182-
{{/if}}
177+
<AiIndicatorWave @loading={{this.searching}} />
183178
</div>
184179
</div>
185180
</div>

assets/stylesheets/common/streaming.scss

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,32 @@ article.streaming .cooked {
2929
animation: flashing 1.5s infinite;
3030
}
3131
}
32+
33+
@keyframes ai-indicator-wave {
34+
0%,
35+
60%,
36+
100% {
37+
transform: initial;
38+
}
39+
30% {
40+
transform: translateY(-0.2em);
41+
}
42+
}
43+
44+
.ai-indicator-wave {
45+
flex: 0 0 auto;
46+
display: inline-flex;
47+
48+
&__dot {
49+
display: inline-block;
50+
@media (prefers-reduced-motion: no-preference) {
51+
animation: ai-indicator-wave 1.8s linear infinite;
52+
}
53+
&:nth-child(2) {
54+
animation-delay: -1.6s;
55+
}
56+
&:nth-child(3) {
57+
animation-delay: -1.4s;
58+
}
59+
}
60+
}

assets/stylesheets/modules/embeddings/common/semantic-search.scss

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
flex-direction: column;
88
align-items: baseline;
99

10+
.ai-indicator-wave {
11+
color: var(--primary-medium);
12+
}
13+
1014
.semantic-search {
1115
&__searching {
1216
display: flex;
@@ -23,22 +27,6 @@
2327
display: inline-block;
2428
margin-left: 3px;
2529
}
26-
27-
&__indicator-wave {
28-
flex: 0 0 auto;
29-
display: inline-flex;
30-
color: var(--primary-medium);
31-
}
32-
&__indicator-dot {
33-
display: inline-block;
34-
animation: ai-summary__indicator-wave 1.8s linear infinite;
35-
&:nth-child(2) {
36-
animation-delay: -1.6s;
37-
}
38-
&:nth-child(3) {
39-
animation-delay: -1.4s;
40-
}
41-
}
4230
}
4331

4432
.semantic-search__entries {

assets/stylesheets/modules/summarization/common/ai-summary.scss

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -157,22 +157,6 @@
157157
display: inline-block;
158158
margin-left: 3px;
159159
}
160-
&__indicator-wave {
161-
flex: 0 0 auto;
162-
display: inline-flex;
163-
}
164-
&__indicator-dot {
165-
display: inline-block;
166-
@media (prefers-reduced-motion: no-preference) {
167-
animation: ai-summary__indicator-wave 1.8s linear infinite;
168-
}
169-
&:nth-child(2) {
170-
animation-delay: -1.6s;
171-
}
172-
&:nth-child(3) {
173-
animation-delay: -1.4s;
174-
}
175-
}
176160
}
177161

178162
.placeholder-summary {
@@ -211,17 +195,6 @@
211195
}
212196
}
213197

214-
@keyframes ai-summary__indicator-wave {
215-
0%,
216-
60%,
217-
100% {
218-
transform: initial;
219-
}
220-
30% {
221-
transform: translateY(-0.2em);
222-
}
223-
}
224-
225198
@keyframes appear {
226199
0% {
227200
opacity: 0;

config/locales/client.en.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ en:
294294
missing_content: "Please enter some content to generate suggestions."
295295
context_menu:
296296
trigger: "Ask AI"
297-
loading: "AI is generating..."
297+
loading: "AI is generating"
298298
cancel: "Cancel"
299299
regen: "Try Again"
300300
confirm: "Confirm"
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { render } from "@ember/test-helpers";
2+
import { module, test } from "qunit";
3+
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
4+
import AiIndicatorWave from "discourse/plugins/discourse-ai/discourse/components/ai-indicator-wave";
5+
6+
module("Integration | Component | ai-indicator-wave", function (hooks) {
7+
setupRenderingTest(hooks);
8+
9+
test("it renders an indicator wave", async function (assert) {
10+
await render(<template><AiIndicatorWave @loading={{true}} /></template>);
11+
assert.dom(".ai-indicator-wave").exists();
12+
});
13+
14+
test("it does not render the indicator wave when loading is false", async function (assert) {
15+
await render(<template><AiIndicatorWave @loading={{false}} /></template>);
16+
assert.dom(".ai-indicator-wave").doesNotExist();
17+
});
18+
});

0 commit comments

Comments
 (0)