diff --git a/assets/javascripts/discourse/connectors/topic-map-expanded-after/ai-summary-box.gjs b/assets/javascripts/discourse/connectors/topic-map-expanded-after/ai-summary-box.gjs index 9052c4b2..4f8717ff 100644 --- a/assets/javascripts/discourse/connectors/topic-map-expanded-after/ai-summary-box.gjs +++ b/assets/javascripts/discourse/connectors/topic-map-expanded-after/ai-summary-box.gjs @@ -19,8 +19,7 @@ import I18n from "discourse-i18n"; import DMenu from "float-kit/components/d-menu"; import DTooltip from "float-kit/components/d-tooltip"; import AiSummarySkeleton from "../../components/ai-summary-skeleton"; - -const STREAMED_TEXT_SPEED = 15; +import smoothStreamText from "../../modifiers/smooth-stream-text"; export default class AiSummaryBox extends Component { @service siteSettings; @@ -36,10 +35,7 @@ export default class AiSummaryBox extends Component { @tracked canRegenerate = false; @tracked loading = false; @tracked isStreaming = false; - @tracked streamedText = ""; - @tracked currentIndex = 0; - typingTimer = null; - streamedTextLength = 0; + @tracked haltAnimation = false; get outdatedSummaryWarningText() { let outdatedText = I18n.t("summary.outdated"); @@ -55,8 +51,6 @@ export default class AiSummaryBox extends Component { } resetSummary() { - this.streamedText = ""; - this.currentIndex = 0; this.text = ""; this.summarizedOn = null; this.summarizedBy = null; @@ -145,26 +139,6 @@ export default class AiSummaryBox extends Component { }); } - typeCharacter() { - if (this.streamedTextLength < this.text.length) { - this.streamedText += this.text.charAt(this.streamedTextLength); - this.streamedTextLength++; - - this.typingTimer = later(this, this.typeCharacter, STREAMED_TEXT_SPEED); - } else { - this.typingTimer = null; - } - } - - onTextUpdate() { - // Reset only if there’s a new summary to process - if (this.typingTimer) { - cancel(this.typingTimer); - } - - this.typeCharacter(); - } - @bind async _updateSummary(update) { const topicSummary = { @@ -173,13 +147,13 @@ export default class AiSummaryBox extends Component { ...update.ai_topic_summary, }; const newText = topicSummary.raw || ""; + this.text = newText; this.loading = false; if (update.done) { this.text = newText; - this.streamedText = newText; - this.displayedTextLength = newText.length; this.isStreaming = false; + this.haltAnimation = true; this.summarizedOn = shortDateNoYear( moment(topicSummary.updated_at, "YYYY-MM-DD HH:mm:ss Z") ); @@ -187,16 +161,6 @@ export default class AiSummaryBox extends Component { this.newPostsSinceSummary = topicSummary.new_posts_since_summary; this.outdated = topicSummary.outdated; this.canRegenerate = topicSummary.outdated && topicSummary.can_regenerate; - - // Clear pending animations - if (this.typingTimer) { - cancel(this.typingTimer); - this.typingTimer = null; - } - } else if (newText.length > this.text.length) { - this.text = newText; - this.isStreaming = true; - this.onTextUpdate(); } } @@ -260,8 +224,13 @@ export default class AiSummaryBox extends Component { {{#if this.loading}} {{else}} -
- +
+ {{#if this.haltAnimation}} + + {{/if}}
{{#if this.summarizedOn}}
diff --git a/assets/javascripts/discourse/modifiers/smooth-stream-text.gjs b/assets/javascripts/discourse/modifiers/smooth-stream-text.gjs new file mode 100644 index 00000000..668922f3 --- /dev/null +++ b/assets/javascripts/discourse/modifiers/smooth-stream-text.gjs @@ -0,0 +1,60 @@ +import { cancel, later } from "@ember/runloop"; +import { htmlSafe } from "@ember/template"; +import Modifier from "ember-modifier"; +import { cook } from "discourse/lib/text"; + +const STREAMED_TEXT_SPEED = 15; + +export default class SmoothStreamTextModifier extends Modifier { + typingTimer = null; + displayedText = ""; + + modify(element, [text, haltAnimation]) { + if (haltAnimation) { + return; + } + this._startTypingAnimation(element, text); + } + + async _startTypingAnimation(element, text) { + if (this.typingTimer) { + cancel(this.typingTimer); + } + + if (this.displayedText.length === 0) { + element.innerHTML = ""; + } + + this._typeCharacter(element, text); + } + + async _typeCharacter(element, text) { + if (this.displayedText.length < text.length) { + this.displayedText += text.charAt(this.displayedText.length); + + try { + const cookedText = await cook(this.displayedText); + element.classList.add("cooked"); + element.innerHTML = htmlSafe(cookedText); + } catch (error) { + console.error("Error cooking text during typing: ", error); + } + + this.typingTimer = later( + this, + this._typeCharacter, + element, + text, + STREAMED_TEXT_SPEED + ); + } else { + this.typingTimer = null; + } + } + + willRemove() { + if (this.typingTimer) { + cancel(this.typingTimer); + } + } +}