From 3dc6a34f792c76d92e8eef5043244caa31ea33f3 Mon Sep 17 00:00:00 2001 From: Keegan George Date: Wed, 26 Mar 2025 15:05:54 -0700 Subject: [PATCH 1/5] UX: Add streaming animation to explain --- .../components/ai-post-helper-menu.gjs | 69 +++++++++++++++++-- 1 file changed, 65 insertions(+), 4 deletions(-) diff --git a/assets/javascripts/discourse/components/ai-post-helper-menu.gjs b/assets/javascripts/discourse/components/ai-post-helper-menu.gjs index ce013a1b3..750eaba9e 100644 --- a/assets/javascripts/discourse/components/ai-post-helper-menu.gjs +++ b/assets/javascripts/discourse/components/ai-post-helper-menu.gjs @@ -3,6 +3,7 @@ import { tracked } from "@glimmer/tracking"; import { action } from "@ember/object"; import didInsert from "@ember/render-modifiers/modifiers/did-insert"; import willDestroy from "@ember/render-modifiers/modifiers/will-destroy"; +import { cancel, later } from "@ember/runloop"; import { service } from "@ember/service"; import { modifier } from "ember-modifier"; import { and } from "truth-helpers"; @@ -10,6 +11,7 @@ import CookText from "discourse/components/cook-text"; import DButton from "discourse/components/d-button"; import FastEdit from "discourse/components/fast-edit"; import FastEditModal from "discourse/components/modal/fast-edit"; +import concatClass from "discourse/helpers/concat-class"; import { ajax } from "discourse/lib/ajax"; import { popupAjaxError } from "discourse/lib/ajax-error"; import { bind } from "discourse/lib/decorators"; @@ -21,6 +23,8 @@ import eq from "truth-helpers/helpers/eq"; import AiHelperLoading from "../components/ai-helper-loading"; import AiHelperOptionsList from "../components/ai-helper-options-list"; +const STREAMED_TEXT_SPEED = 15; + export default class AiPostHelperMenu extends Component { @service messageBus; @service site; @@ -43,6 +47,11 @@ export default class AiPostHelperMenu extends Component { @tracked isSavingFootnote = false; @tracked supportsAddFootnote = this.args.data.supportsFastEdit; + @tracked isStreaming = false; + @tracked streamedText = ""; + typingTimer = null; + streamedTextLength = 0; + MENU_STATES = { options: "OPTIONS", loading: "LOADING", @@ -157,6 +166,34 @@ export default class AiPostHelperMenu extends Component { return sanitize(text); } + typeCharacter() { + if (this.streamedTextLength < this.suggestion.length) { + this.streamedText += this.suggestion.charAt(this.streamedTextLength); + this.streamedTextLength++; + + this.typingTimer = later(this, this.typeCharacter, STREAMED_TEXT_SPEED); + } else { + this.typingTimer = null; + } + } + + onTextUpdate() { + this.cancelTypingTimer(); + this.typeCharacter(); + } + + cancelTypingTimer() { + if (this.typingTimer) { + cancel(this.typingTimer); + } + } + + resetStreaming() { + this.cancelTypingTimer(); + this.streamedText = ""; + this.streamedTextLength = 0; + } + @bind subscribe() { const channel = `/discourse-ai/ai-helper/stream_suggestion/${this.args.data.quoteState.postId}`; @@ -172,9 +209,22 @@ export default class AiPostHelperMenu extends Component { } @bind - _updateResult(result) { + async _updateResult(result) { this.streaming = !result.done; - this.suggestion = result.result; + const newText = result.result; + + if (result.done) { + this.streamedText = newText; + this.isStreaming = false; + this.suggestion = newText; + + // Clear pending animations + this.cancelTypingTimer(); + } else if (newText.length > this.suggestion.length) { + this.suggestion = newText; + this.isStreaming = true; + await this.onTextUpdate(); + } } @action @@ -323,6 +373,10 @@ export default class AiPostHelperMenu extends Component { } } + get renderedText() { + return this.isStreaming ? this.streamedText : this.suggestion; + } +