@@ -2,39 +2,20 @@ import Component from "@glimmer/component";
22import { tracked } from " @glimmer/tracking" ;
33import { action } from " @ember/object" ;
44import didInsert from " @ember/render-modifiers/modifiers/did-insert" ;
5+ import didUpdate from " @ember/render-modifiers/modifiers/did-update" ;
56import willDestroy from " @ember/render-modifiers/modifiers/will-destroy" ;
67import { cancel , later } from " @ember/runloop" ;
78import { service } from " @ember/service" ;
89import CookText from " discourse/components/cook-text" ;
910import DButton from " discourse/components/d-button" ;
11+ import concatClass from " discourse/helpers/concat-class" ;
1012import { ajax } from " discourse/lib/ajax" ;
1113import { bind } from " discourse/lib/decorators" ;
1214import { i18n } from " discourse-i18n" ;
1315import AiBlinkingAnimation from " ./ai-blinking-animation" ;
1416
1517const DISCOVERY_TIMEOUT_MS = 10000 ;
16- const BUFFER_WORDS_COUNT = 50 ;
17-
18- function setUpBuffer (discovery , bufferTarget ) {
19- const paragraphs = discovery .split (/ \n + / );
20- let wordCount = 0 ;
21- const paragraphBuffer = [];
22-
23- for (const paragraph of paragraphs) {
24- const wordsInParagraph = paragraph .split (/ \s + / );
25- wordCount += wordsInParagraph .length ;
26-
27- if (wordCount >= bufferTarget) {
28- paragraphBuffer .push (paragraph .concat (" ..." ));
29- return paragraphBuffer .join (" \n " );
30- } else {
31- paragraphBuffer .push (paragraph);
32- paragraphBuffer .push (" \n " );
33- }
34- }
35-
36- return null ;
37- }
18+ const STREAMED_TEXT_SPEED = 23 ;
3819
3920export default class AiSearchDiscoveries extends Component {
4021 @service search;
@@ -43,9 +24,36 @@ export default class AiSearchDiscoveries extends Component {
4324
4425 @tracked loadingDiscoveries = false ;
4526 @tracked hideDiscoveries = false ;
46- @tracked fulldiscoveryToggled = false ;
27+ @tracked fullDiscoveryToggled = false ;
28+ @tracked discoveryPreviewLength = this .args .discoveryPreviewLength || 150 ;
29+
30+ @tracked isStreaming = false ;
31+ @tracked streamedText = " " ;
4732
4833 discoveryTimeout = null ;
34+ typingTimer = null ;
35+ streamedTextLength = 0 ;
36+
37+ typeCharacter () {
38+ if (this .streamedTextLength < this .discobotDiscoveries .discovery .length ) {
39+ this .streamedText += this .discobotDiscoveries .discovery .charAt (
40+ this .streamedTextLength
41+ );
42+ this .streamedTextLength ++ ;
43+
44+ this .typingTimer = later (this , this .typeCharacter , STREAMED_TEXT_SPEED );
45+ } else {
46+ this .typingTimer = null ;
47+ }
48+ }
49+
50+ onTextUpdate () {
51+ if (this .typingTimer ) {
52+ cancel (this .typingTimer );
53+ }
54+
55+ this .typeCharacter ();
56+ }
4957
5058 @bind
5159 async _updateDiscovery (update ) {
@@ -54,28 +62,29 @@ export default class AiSearchDiscoveries extends Component {
5462 cancel (this .discoveryTimeout );
5563 }
5664
57- if (! this .discobotDiscoveries .discoveryPreview ) {
58- const buffered = setUpBuffer (
59- update .ai_discover_reply ,
60- BUFFER_WORDS_COUNT
61- );
62- if (buffered) {
63- this .discobotDiscoveries .discoveryPreview = buffered;
64- this .loadingDiscoveries = false ;
65- }
65+ if (! this .discobotDiscoveries .discovery ) {
66+ this .discobotDiscoveries .discovery = " " ;
6667 }
6768
69+ const newText = update .ai_discover_reply ;
6870 this .discobotDiscoveries .modelUsed = update .model_used ;
69- this .discobotDiscoveries . discovery = update . ai_discover_reply ;
71+ this .loadingDiscoveries = false ;
7072
7173 // Handling short replies.
7274 if (update .done ) {
73- if (! this .discobotDiscoveries .discoveryPreview ) {
74- this .discobotDiscoveries .discoveryPreview = update .ai_discover_reply ;
75+ this .discobotDiscoveries .discovery = newText;
76+ this .streamedText = newText;
77+ this .isStreaming = false ;
78+
79+ // Clear pending animations
80+ if (this .typingTimer ) {
81+ cancel (this .typingTimer );
82+ this .typingTimer = null ;
7583 }
76-
77- this .discobotDiscoveries .discovery = update .ai_discover_reply ;
78- this .loadingDiscoveries = false ;
84+ } else if (newText .length > this .discobotDiscoveries .discovery .length ) {
85+ this .discobotDiscoveries .discovery = newText;
86+ this .isStreaming = true ;
87+ await this .onTextUpdate ();
7988 }
8089 }
8190 }
@@ -101,29 +110,38 @@ export default class AiSearchDiscoveries extends Component {
101110 }
102111
103112 get toggleLabel () {
104- if (this .fulldiscoveryToggled ) {
113+ if (this .fullDiscoveryToggled ) {
105114 return " discourse_ai.discobot_discoveries.collapse" ;
106115 } else {
107116 return " discourse_ai.discobot_discoveries.tell_me_more" ;
108117 }
109118 }
110119
111120 get toggleIcon () {
112- if (this .fulldiscoveryToggled ) {
121+ if (this .fullDiscoveryToggled ) {
113122 return " chevron-up" ;
114123 } else {
115124 return " " ;
116125 }
117126 }
118127
119- get toggleMakesSense () {
128+ get canShowExpandtoggle () {
120129 return (
121- this .discobotDiscoveries .discoveryPreview &&
122- this .discobotDiscoveries .discoveryPreview !==
123- this .discobotDiscoveries .discovery
130+ ! this .loadingDiscoveries &&
131+ this .renderedDiscovery .length > this .discoveryPreviewLength
124132 );
125133 }
126134
135+ get renderedDiscovery () {
136+ return this .isStreaming
137+ ? this .streamedText
138+ : this .discobotDiscoveries .discovery ;
139+ }
140+
141+ get renderPreviewOnly () {
142+ return ! this .fullDiscoveryToggled && this .canShowExpandtoggle ;
143+ }
144+
127145 @action
128146 async triggerDiscovery () {
129147 if (this .discobotDiscoveries .lastQuery === this .query ) {
@@ -153,12 +171,11 @@ export default class AiSearchDiscoveries extends Component {
153171
154172 @action
155173 toggleDiscovery () {
156- this .fulldiscoveryToggled = ! this .fulldiscoveryToggled ;
174+ this .fullDiscoveryToggled = ! this .fullDiscoveryToggled ;
157175 }
158176
159177 timeoutDiscovery () {
160178 this .loadingDiscoveries = false ;
161- this .discobotDiscoveries .discoveryPreview = " " ;
162179 this .discobotDiscoveries .discovery = " " ;
163180
164181 this .discobotDiscoveries .discoveryTimedOut = true ;
@@ -167,7 +184,8 @@ export default class AiSearchDiscoveries extends Component {
167184 <template >
168185 <div
169186 class =" ai-search-discoveries"
170- {{didInsert this . subscribe}}
187+ {{didInsert this . subscribe @ searchTerm}}
188+ {{didUpdate this . subscribe @ searchTerm}}
171189 {{didInsert this . triggerDiscovery this . query}}
172190 {{willDestroy this . unsubscribe}}
173191 >
@@ -177,19 +195,20 @@ export default class AiSearchDiscoveries extends Component {
177195 {{else if this . discobotDiscoveries.discoveryTimedOut }}
178196 {{i18n " discourse_ai.discobot_discoveries.timed_out" }}
179197 {{else }}
180- {{#if this . fulldiscoveryToggled }}
181- <div class =" ai-search-discoveries__discovery-full cooked" >
182- <CookText @ rawText ={{this .discobotDiscoveries.discovery }} />
183- </div >
184- {{else }}
185- <div class =" ai-search-discoveries__discovery-preview cooked" >
186- <CookText
187- @ rawText ={{this .discobotDiscoveries.discoveryPreview }}
188- />
198+ <article
199+ class ={{concatClass
200+ " ai-search-discoveries__discovery"
201+ ( if this . renderPreviewOnly " preview" )
202+ ( if this . isStreaming " streaming" )
203+ " streamable-content"
204+ }}
205+ >
206+ <div class =" cooked" >
207+ <CookText @ rawText ={{this .renderedDiscovery }} />
189208 </div >
190- {{/ if }}
209+ </ article >
191210
192- {{#if this . toggleMakesSense }}
211+ {{#if this . canShowExpandtoggle }}
193212 <DButton
194213 class =" btn-flat btn-text ai-search-discoveries__toggle"
195214 @ label ={{this .toggleLabel }}
0 commit comments