@@ -57,11 +57,17 @@ const ICONS = {
57
57
// https://github.com/n3r4zzurr0/svg-spinners/blob/main/svg-css/3-dots-fade.svg
58
58
dots_fade :
59
59
'<svg width="24" height="24" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>.spinner_S1WN{animation:spinner_MGfb .8s linear infinite;animation-delay:-.8s}.spinner_Km9P{animation-delay:-.65s}.spinner_JApP{animation-delay:-.5s}@keyframes spinner_MGfb{93.75%,100%{opacity:.2}}</style><circle class="spinner_S1WN" cx="4" cy="12" r="3"/><circle class="spinner_S1WN spinner_Km9P" cx="12" cy="12" r="3"/><circle class="spinner_S1WN spinner_JApP" cx="20" cy="12" r="3"/></svg>' ,
60
- // https://github.com/n3r4zzurr0/svg-spinners/blob/main/svg-css/bouncing-ball.svg
61
- ball_bounce :
62
- '<svg width="24" height="24" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>.spinner_rXNP{animation:spinner_YeBj .8s infinite; opacity:.8}@keyframes spinner_YeBj{0%{animation-timing-function:cubic-bezier(0.33,0,.66,.33);cy:5px}46.875%{cy:20px;rx:4px;ry:4px}50%{animation-timing-function:cubic-bezier(0.33,.66,.66,1);cy:20.5px;rx:4.8px;ry:3px}53.125%{rx:4px;ry:4px}100%{cy:5px}}</style><ellipse class="spinner_rXNP" cx="12" cy="5" rx="4" ry="4"/></svg>' ,
60
+ dot : '<svg width="12" height="12" xmlns="http://www.w3.org/2000/svg" class="chat-streaming-dot" style="margin-left:.25em;margin-top:-.25em"><circle cx="6" cy="6" r="6"/></svg>' ,
63
61
} ;
64
62
63
+ function createSVGIcon ( icon : string ) : HTMLElement {
64
+ const parser = new DOMParser ( ) ;
65
+ const svgDoc = parser . parseFromString ( icon , "image/svg+xml" ) ;
66
+ return svgDoc . documentElement ;
67
+ }
68
+
69
+ const SVG_DOT = createSVGIcon ( ICONS . dot ) ;
70
+
65
71
const requestScroll = ( el : HTMLElement , cancelIfScrolledUp = false ) => {
66
72
el . dispatchEvent (
67
73
new CustomEvent ( "shiny-chat-request-scroll" , {
@@ -127,17 +133,29 @@ class ChatMessage extends LightElement {
127
133
return html `
128
134
< div class ="message-icon "> ${ unsafeHTML ( icon ) } </ div >
129
135
< div class ="message-content "> ${ content } </ div >
130
- < div class ="message-streaming-icon "> ${ unsafeHTML ( ICONS . ball_bounce ) } </ div >
131
136
` ;
132
137
}
133
138
134
139
updated ( changedProperties : Map < string , unknown > ) : void {
135
140
if ( changedProperties . has ( "content" ) ) {
136
141
this . #highlightAndCodeCopy( ) ;
142
+ if ( this . streaming ) this . #appendStreamingDot( ) ;
137
143
// It's important that the scroll request happens at this point in time, since
138
144
// otherwise, the content may not be fully rendered yet
139
145
requestScroll ( this , this . streaming ) ;
140
146
}
147
+ if ( changedProperties . has ( "streaming" ) ) {
148
+ this . streaming ? this . #appendStreamingDot( ) : this . #removeStreamingDot( ) ;
149
+ }
150
+ }
151
+
152
+ #appendStreamingDot( ) : void {
153
+ const content = this . querySelector ( ".message-content" ) as HTMLElement ;
154
+ content . lastElementChild ?. appendChild ( SVG_DOT ) ;
155
+ }
156
+
157
+ #removeStreamingDot( ) : void {
158
+ this . querySelector ( ".message-content svg.chat-streaming-dot" ) ?. remove ( ) ;
141
159
}
142
160
143
161
// Highlight code blocks after the element is rendered
@@ -416,6 +434,7 @@ class ChatContainer extends LightElement {
416
434
lastMessage . setAttribute ( "content" , message . content ) ;
417
435
418
436
if ( message . chunk_type === "message_end" ) {
437
+ this . lastMessage ?. removeAttribute ( "streaming" ) ;
419
438
this . #finalizeMessage( ) ;
420
439
}
421
440
}
@@ -441,7 +460,6 @@ class ChatContainer extends LightElement {
441
460
442
461
#finalizeMessage( ) : void {
443
462
this . input . disabled = false ;
444
- this . lastMessage ?. removeAttribute ( "streaming" ) ;
445
463
}
446
464
447
465
#onRequestScroll( event : CustomEvent < requestScrollEvent > ) : void {
0 commit comments