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

Commit c421f71

Browse files
authored
DEV: Handle streaming animation within AiSummaryBox (#901)
This PR further decouples the streaming animation by completely handling the streaming animation directly in the `AiSummaryBox` component. Previously, handling the streaming animation by calling methods in the `ai-streamer` API was leading to timing issues making things out-of-sync. This results in some issues such as the last update of streamed text not being shown. Handling streaming directly in the component should simplify things drastically and prevent any issues.
1 parent 021e096 commit c421f71

File tree

3 files changed

+51
-67
lines changed

3 files changed

+51
-67
lines changed

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

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import { action } from "@ember/object";
55
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
66
import didUpdate from "@ember/render-modifiers/modifiers/did-update";
77
import willDestroy from "@ember/render-modifiers/modifiers/will-destroy";
8-
import { next } from "@ember/runloop";
8+
import { cancel, later } from "@ember/runloop";
99
import { service } from "@ember/service";
10+
import CookText from "discourse/components/cook-text";
1011
import DButton from "discourse/components/d-button";
1112
import concatClass from "discourse/helpers/concat-class";
1213
import { ajax } from "discourse/lib/ajax";
@@ -18,8 +19,8 @@ import I18n from "discourse-i18n";
1819
import DMenu from "float-kit/components/d-menu";
1920
import DTooltip from "float-kit/components/d-tooltip";
2021
import AiSummarySkeleton from "../../components/ai-summary-skeleton";
21-
import streamUpdaterText from "../../lib/ai-streamer/progress-handlers";
22-
import SummaryUpdater from "../../lib/ai-streamer/updaters/summary-updater";
22+
23+
const STREAMED_TEXT_SPEED = 15;
2324

2425
export default class AiSummaryBox extends Component {
2526
@service siteSettings;
@@ -35,8 +36,10 @@ export default class AiSummaryBox extends Component {
3536
@tracked canRegenerate = false;
3637
@tracked loading = false;
3738
@tracked isStreaming = false;
38-
oldRaw = null; // used for comparison in SummaryUpdater in lib/ai-streamer/updaters
39-
finalSummary = null;
39+
@tracked streamedText = "";
40+
@tracked currentIndex = 0;
41+
typingTimer = null;
42+
streamedTextLength = 0;
4043

4144
get outdatedSummaryWarningText() {
4245
let outdatedText = I18n.t("summary.outdated");
@@ -52,6 +55,8 @@ export default class AiSummaryBox extends Component {
5255
}
5356

5457
resetSummary() {
58+
this.streamedText = "";
59+
this.currentIndex = 0;
5560
this.text = "";
5661
this.summarizedOn = null;
5762
this.summarizedBy = null;
@@ -83,8 +88,7 @@ export default class AiSummaryBox extends Component {
8388
}
8489
const channel = `/discourse-ai/summaries/topic/${this.args.outletArgs.topic.id}`;
8590
this._channel = channel;
86-
// we attempt to recover the last message in the bus so we subscrcibe at -2
87-
this.messageBus.subscribe(channel, this._updateSummary, -2);
91+
this.messageBus.subscribe(channel, this._updateSummary);
8892
}
8993

9094
@bind
@@ -134,36 +138,63 @@ export default class AiSummaryBox extends Component {
134138
return ajax(url).then((data) => {
135139
if (data?.ai_topic_summary?.summarized_text) {
136140
data.done = true;
137-
// Our streamer won't display the summary unless the summary box is in the DOM.
138-
// Wait for the next runloop or cached summaries won't appear.
139-
next(() => this._updateSummary(data));
141+
this._updateSummary(data);
140142
}
141143
});
142144
}
143145

146+
typeCharacter() {
147+
if (this.streamedTextLength < this.text.length) {
148+
this.streamedText += this.text.charAt(this.streamedTextLength);
149+
this.streamedTextLength++;
150+
151+
this.typingTimer = later(this, this.typeCharacter, STREAMED_TEXT_SPEED);
152+
} else {
153+
this.typingTimer = null;
154+
}
155+
}
156+
157+
onTextUpdate() {
158+
// Reset only if there’s a new summary to process
159+
if (this.typingTimer) {
160+
cancel(this.typingTimer);
161+
}
162+
163+
this.typeCharacter();
164+
}
165+
144166
@bind
145-
_updateSummary(update) {
167+
async _updateSummary(update) {
146168
const topicSummary = {
147169
done: update.done,
148170
raw: update.ai_topic_summary?.summarized_text,
149171
...update.ai_topic_summary,
150172
};
173+
const newText = topicSummary.raw || "";
151174
this.loading = false;
152175

153-
this.isStreaming = true;
154-
streamUpdaterText(SummaryUpdater, topicSummary, this);
155-
156176
if (update.done) {
177+
this.text = newText;
178+
this.streamedText = newText;
179+
this.displayedTextLength = newText.length;
157180
this.isStreaming = false;
158-
this.text = this.finalSummary;
159181
this.summarizedOn = shortDateNoYear(
160182
moment(topicSummary.updated_at, "YYYY-MM-DD HH:mm:ss Z")
161183
);
162184
this.summarizedBy = topicSummary.algorithm;
163185
this.newPostsSinceSummary = topicSummary.new_posts_since_summary;
164186
this.outdated = topicSummary.outdated;
165-
this.newPostsSinceSummary = topicSummary.new_posts_since_summary;
166187
this.canRegenerate = topicSummary.outdated && topicSummary.can_regenerate;
188+
189+
// Clear pending animations
190+
if (this.typingTimer) {
191+
cancel(this.typingTimer);
192+
this.typingTimer = null;
193+
}
194+
} else if (newText.length > this.text.length) {
195+
this.text = newText;
196+
this.isStreaming = true;
197+
this.onTextUpdate();
167198
}
168199
}
169200

@@ -228,7 +259,7 @@ export default class AiSummaryBox extends Component {
228259
<AiSummarySkeleton />
229260
{{else}}
230261
<div class="generated-summary cooked">
231-
{{this.text}}
262+
<CookText @rawText={{this.streamedText}} />
232263
</div>
233264
{{#if this.summarizedOn}}
234265
<div class="summarized-on">

assets/javascripts/discourse/lib/ai-streamer/updaters/summary-updater.js

Lines changed: 0 additions & 47 deletions
This file was deleted.

test/javascripts/acceptance/topic-summary-test.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { click, visit } from "@ember/test-helpers";
2-
import { skip, test } from "qunit";
2+
import { test } from "qunit";
33
import topicFixtures from "discourse/tests/fixtures/topic";
44
import {
55
acceptance,
@@ -30,7 +30,7 @@ acceptance("Topic - Summary", function (needs) {
3030
updateCurrentUser({ id: currentUserId });
3131
});
3232

33-
skip("displays streamed summary", async function (assert) {
33+
test("displays streamed summary", async function (assert) {
3434
await visit("/t/-/1");
3535

3636
const partialSummary = "This a";
@@ -67,7 +67,7 @@ acceptance("Topic - Summary", function (needs) {
6767
.exists("summary metadata exists");
6868
});
6969

70-
skip("clicking summary links", async function (assert) {
70+
test("clicking summary links", async function (assert) {
7171
await visit("/t/-/1");
7272

7373
const partialSummary = "In this post,";

0 commit comments

Comments
 (0)