1- import { LitElement , html } from "lit" ;
1+ import { LitElement , css , html } from "lit" ;
22import { unsafeHTML } from "lit-html/directives/unsafe-html.js" ;
33import { property } from "lit/decorators.js" ;
44
@@ -57,23 +57,63 @@ const ICONS = {
5757 '<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>' ,
5858} ;
5959
60- class ChatMessage extends LightElement {
60+ class ChatMessage extends LitElement {
6161 @property ( ) content = "..." ;
6262 @property ( ) content_type : ContentType = "markdown" ;
6363 @property ( { type : Boolean , reflect : true } ) streaming = false ;
64- @property ( ) icon = "" ;
6564
66- render ( ) {
67- let msg_icon = ICONS . dots_fade ;
65+ static styles = css `
66+ :host {
67+ display: grid;
68+ grid-template-columns: auto minmax(0, 1fr);
69+ gap: 1rem;
70+ align-items: start;
71+ }
6872
69- // Show dots until we have content
70- const hasContent = this . content . trim ( ) . length !== 0 ;
71- if ( hasContent ) {
72- msg_icon = this . icon || ICONS . robot ;
73+ :host > * {
74+ height: fit-content;
75+ }
76+
77+ .message-icon {
78+ border-radius: 50%;
79+ border: var(--shiny-chat-border);
80+ height: 2rem;
81+ width: 2rem;
82+ display: grid;
83+ place-items: center;
7384 }
7485
86+ slot[name="icon"] > * {
87+ height: 20px;
88+ width: 20px;
89+ margin: 0 !important;
90+ max-height: 85%;
91+ max-width: 85%;
92+ object-fit: contain;
93+ }
94+
95+ /* Vertically center the 2nd column (message content) */
96+ shiny-markdown-stream {
97+ align-self: center;
98+
99+ p:first-child {
100+ margin-block-start: 0;
101+ }
102+ p:last-child {
103+ margin-block-end: 0;
104+ }
105+ }
106+ ` ;
107+
108+ render ( ) {
109+ // Show dots until we have content
110+ const hasContent = this . content . trim ( ) . length > 0 ;
111+ const defaultIcon = hasContent ? ICONS . robot : ICONS . dots_fade ;
112+
75113 return html `
76- < div class ="message-icon "> ${ unsafeHTML ( msg_icon ) } </ div >
114+ < div class ="message-icon ">
115+ < slot name ="icon "> ${ unsafeHTML ( defaultIcon ) } </ slot >
116+ </ div >
77117 < shiny-markdown-stream
78118 content =${ this . content }
79119 content-type =${ this . content_type }
@@ -268,9 +308,7 @@ class ChatInput extends LightElement {
268308 }
269309}
270310
271- class ChatContainer extends LightElement {
272- @property ( ) icon = "" ;
273-
311+ class ChatContainer extends LitElement {
274312 private get input ( ) : ChatInput {
275313 return this . querySelector ( CHAT_INPUT_TAG ) as ChatInput ;
276314 }
@@ -284,8 +322,30 @@ class ChatContainer extends LightElement {
284322 return last ? ( last as ChatMessage ) : null ;
285323 }
286324
325+ private get iconAssistant ( ) : Element | void {
326+ const slot = this . shadowRoot ?. querySelector (
327+ 'slot[name="icon-assistant"]'
328+ ) as HTMLSlotElement ;
329+
330+ if ( ! slot ) return ;
331+
332+ let icon : Element | undefined | null = slot . assignedElements ( ) [ 0 ] ;
333+ if ( ! icon ) return ;
334+
335+ if ( icon ?. matches ( ".icon-container" ) ) {
336+ // From Python/R we use a wrapper element because users may give raw HTML
337+ icon = icon . firstElementChild ;
338+ }
339+
340+ return icon ? icon : undefined ;
341+ }
342+
287343 render ( ) {
288- return html `` ;
344+ return html `
345+ < slot name ="icon-assistant " style ="display: none "> </ slot >
346+ < slot name ="messages "> </ slot >
347+ < slot name ="input "> </ slot >
348+ ` ;
289349 }
290350
291351 firstUpdated ( ) : void {
@@ -351,17 +411,42 @@ class ChatContainer extends LightElement {
351411 }
352412 }
353413
414+ #messageIcon( message : Message ) : HTMLElement | undefined {
415+ if ( message . role === "user" ) return ;
416+
417+ let icon : HTMLElement | undefined ;
418+ if ( message . icon ) {
419+ icon = document . createElement ( "div" ) ;
420+ icon . innerHTML = message . icon ;
421+ if ( icon . firstChild ) {
422+ icon = icon . firstChild . cloneNode ( ) as HTMLElement ;
423+ } else {
424+ icon = undefined ;
425+ }
426+ }
427+
428+ if ( ! icon && this . iconAssistant ) {
429+ icon = this . iconAssistant . cloneNode ( true ) as HTMLElement ;
430+ }
431+
432+ if ( ! icon ) return ;
433+ icon . setAttribute ( "slot" , "icon" ) ;
434+ return icon ;
435+ }
436+
354437 #appendMessage( message : Message , finalize = true ) : void {
355438 this . #initMessage( ) ;
356439
357440 const TAG_NAME =
358441 message . role === "user" ? CHAT_USER_MESSAGE_TAG : CHAT_MESSAGE_TAG ;
359442
360- if ( this . icon ) {
361- message . icon = message . icon || this . icon ;
443+ const msg = createElement ( TAG_NAME , message ) ;
444+
445+ const icon = this . #messageIcon( message ) ;
446+ if ( icon ) {
447+ msg . appendChild ( icon ) ;
362448 }
363449
364- const msg = createElement ( TAG_NAME , message ) ;
365450 this . messages . appendChild ( msg ) ;
366451
367452 if ( finalize ) {
0 commit comments