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

Commit 37c2930

Browse files
authored
FIX: Decouple DOM manipulation from SummaryStreamer (#844)
Previously, when we added smooth streaming animation to summarization (#778) we used the same logic and lib we did for AI Bot. However, since `AiSummaryBox` is an Ember component, the direct DOM manipulation done in the streamer (`SummaryUpdater`) would often result in issues with summarization where sometimes summarization updates would hang, especially on the last result. This is likely due to the DOM manipulation being done in the streamer being incongruent with Ember's way of rendering. In this PR, we remove the direct DOM manipulation done in the lib `SummaryUpdater` in favour of directly updating the properties in `AiSummaryBox` using the `componentContext`. Instead of messing with Ember's rendered DOM, passing the updates and allowing the component to render the updates directly should likely prevent further issues with summarization. The bug itself is quite difficult to repro and also difficult to test, so no tests have been added to this PR. But I will be manually testing and assessing for any potential issues.
1 parent e768fa8 commit 37c2930

File tree

3 files changed

+35
-28
lines changed

3 files changed

+35
-28
lines changed

assets/javascripts/discourse/connectors/topic-map-expanded-after/ai-summary-box.gjs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import willDestroy from "@ember/render-modifiers/modifiers/will-destroy";
88
import { next } from "@ember/runloop";
99
import { service } from "@ember/service";
1010
import DButton from "discourse/components/d-button";
11+
import concatClass from "discourse/helpers/concat-class";
1112
import { ajax } from "discourse/lib/ajax";
1213
import { shortDateNoYear } from "discourse/lib/formatter";
1314
import dIcon from "discourse-common/helpers/d-icon";
@@ -32,6 +33,7 @@ export default class AiSummaryBox extends Component {
3233
@tracked outdated = false;
3334
@tracked canRegenerate = false;
3435
@tracked loading = false;
36+
@tracked isStreaming = false;
3537
oldRaw = null; // used for comparison in SummaryUpdater in lib/ai-streamer
3638
finalSummary = null;
3739

@@ -147,9 +149,11 @@ export default class AiSummaryBox extends Component {
147149
};
148150
this.loading = false;
149151

152+
this.isStreaming = true;
150153
streamSummaryText(topicSummary, this);
151154

152155
if (update.done) {
156+
this.isStreaming = false;
153157
this.text = this.finalSummary;
154158
this.summarizedOn = shortDateNoYear(
155159
moment(topicSummary.updated_at, "YYYY-MM-DD HH:mm:ss Z")
@@ -212,11 +216,18 @@ export default class AiSummaryBox extends Component {
212216
{{/if}}
213217
</header>
214218

215-
<article class="ai-summary-box">
219+
<article
220+
class={{concatClass
221+
"ai-summary-box"
222+
(if this.isStreaming "streaming")
223+
}}
224+
>
216225
{{#if this.loading}}
217226
<AiSummarySkeleton />
218227
{{else}}
219-
<div class="generated-summary cooked">{{this.text}}</div>
228+
<div class="generated-summary cooked">
229+
{{this.text}}
230+
</div>
220231
{{#if this.summarizedOn}}
221232
<div class="summarized-on">
222233
<p>

assets/javascripts/discourse/lib/ai-streamer.js

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -152,9 +152,9 @@ export class SummaryUpdater extends StreamUpdater {
152152
set streaming(value) {
153153
if (this.element) {
154154
if (value) {
155-
this.element.classList.add("streaming");
155+
this.componentContext.isStreaming = true;
156156
} else {
157-
this.element.classList.remove("streaming");
157+
this.componentContext.isStreaming = false;
158158
}
159159
}
160160
}
@@ -163,27 +163,15 @@ export class SummaryUpdater extends StreamUpdater {
163163
this.componentContext.oldRaw = value;
164164
const cooked = await cook(value);
165165

166-
// resets animation
167-
this.element.classList.remove("streaming");
168-
void this.element.offsetWidth;
169-
this.element.classList.add("streaming");
170-
171-
const cookedElement = document.createElement("div");
172-
cookedElement.innerHTML = cooked;
173-
174-
if (!done) {
175-
addProgressDot(cookedElement);
176-
}
177-
await this.setCooked(cookedElement.innerHTML);
166+
await this.setCooked(cooked);
178167

179168
if (done) {
180169
this.componentContext.finalSummary = cooked;
181170
}
182171
}
183172

184173
async setCooked(value) {
185-
const cookedContainer = this.element.querySelector(".generated-summary");
186-
cookedContainer.innerHTML = value;
174+
this.componentContext.text = value;
187175
}
188176

189177
get raw() {

assets/stylesheets/common/streaming.scss

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,26 @@
88
}
99
}
1010

11+
@mixin progress-dot {
12+
content: "\25CF";
13+
font-family: Söhne Circle, system-ui, -apple-system, Segoe UI, Roboto, Ubuntu,
14+
Cantarell, Noto Sans, sans-serif;
15+
line-height: normal;
16+
margin-left: 0.25rem;
17+
vertical-align: baseline;
18+
animation: flashing 1.5s 3s infinite;
19+
display: inline-block;
20+
font-size: 1rem;
21+
color: var(--tertiary-medium);
22+
}
23+
24+
.streaming .cooked p:last-child::after {
25+
@include progress-dot;
26+
}
27+
1128
article.streaming .cooked {
1229
.progress-dot::after {
13-
content: "\25CF";
14-
font-family: Söhne Circle, system-ui, -apple-system, Segoe UI, Roboto,
15-
Ubuntu, Cantarell, Noto Sans, sans-serif;
16-
line-height: normal;
17-
margin-left: 0.25rem;
18-
vertical-align: baseline;
19-
animation: flashing 1.5s 3s infinite;
20-
display: inline-block;
21-
font-size: 1rem;
22-
color: var(--tertiary-medium);
30+
@include progress-dot;
2331
}
2432

2533
> .progress-dot:only-child::after {

0 commit comments

Comments
 (0)