@@ -2,12 +2,9 @@ import { LitElement, html } from "lit";
22import { unsafeHTML } from "lit-html/directives/unsafe-html.js" ;
33import { property } from "lit/decorators.js" ;
44
5- import ClipboardJS from "clipboard" ;
6- import { sanitize } from "dompurify" ;
7- import hljs from "highlight.js/lib/common" ;
8- import { Renderer , parse } from "marked" ;
5+ import { contentToHTML } from "../markdown-stream/markdown-stream" ;
96
10- import { createElement } from "./_utils" ;
7+ import { LightElement , createElement } from "../utils /_utils" ;
118
129type ContentType = "markdown" | "html" | "text" ;
1310
@@ -57,17 +54,8 @@ const ICONS = {
5754 // https://github.com/n3r4zzurr0/svg-spinners/blob/main/svg-css/3-dots-fade.svg
5855 dots_fade :
5956 '<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- 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>' ,
6157} ;
6258
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-
7159const requestScroll = ( el : HTMLElement , cancelIfScrolledUp = false ) => {
7260 el . dispatchEvent (
7361 new CustomEvent ( "shiny-chat-request-scroll" , {
@@ -78,112 +66,24 @@ const requestScroll = (el: HTMLElement, cancelIfScrolledUp = false) => {
7866 ) ;
7967} ;
8068
81- // For rendering chat output, we use typical Markdown behavior of passing through raw
82- // HTML (albeit sanitizing afterwards).
83- //
84- // For echoing chat input, we escape HTML. This is not for security reasons but just
85- // because it's confusing if the user is using tag-like syntax to demarcate parts of
86- // their prompt for other reasons (like <User>/<Assistant> for providing examples to the
87- // chat model), and those tags simply vanish.
88- const rendererEscapeHTML = new Renderer ( ) ;
89- rendererEscapeHTML . html = ( html : string ) =>
90- html
91- . replaceAll ( "&" , "&" )
92- . replaceAll ( "<" , "<" )
93- . replaceAll ( ">" , ">" )
94- . replaceAll ( '"' , """ )
95- . replaceAll ( "'" , "'" ) ;
96- const markedEscapeOpts = { renderer : rendererEscapeHTML } ;
97-
98- function contentToHTML (
99- content : string ,
100- content_type : ContentType | "semi-markdown"
101- ) {
102- if ( content_type === "markdown" ) {
103- return unsafeHTML ( sanitize ( parse ( content ) as string ) ) ;
104- } else if ( content_type === "semi-markdown" ) {
105- return unsafeHTML ( sanitize ( parse ( content , markedEscapeOpts ) as string ) ) ;
106- } else if ( content_type === "html" ) {
107- return unsafeHTML ( sanitize ( content ) ) ;
108- } else if ( content_type === "text" ) {
109- return content ;
110- } else {
111- throw new Error ( `Unknown content type: ${ content_type } ` ) ;
112- }
113- }
114-
115- // https://lit.dev/docs/components/shadow-dom/#implementing-createrenderroot
116- class LightElement extends LitElement {
117- createRenderRoot ( ) {
118- return this ;
119- }
120- }
121-
12269class ChatMessage extends LightElement {
123- @property ( ) content = "" ;
70+ @property ( ) content = "... " ;
12471 @property ( ) content_type : ContentType = "markdown" ;
12572 @property ( { type : Boolean , reflect : true } ) streaming = false ;
12673
12774 render ( ) : ReturnType < LitElement [ "render" ] > {
128- const content = contentToHTML ( this . content , this . content_type ) ;
129-
13075 const noContent = this . content . trim ( ) . length === 0 ;
13176 const icon = noContent ? ICONS . dots_fade : ICONS . robot ;
13277
13378 return html `
13479 < div class ="message-icon "> ${ unsafeHTML ( icon ) } </ div >
135- < div class ="message-content "> ${ content } </ div >
80+ < shiny-markdown
81+ content =${ this . content }
82+ content_type =${ this . content_type }
83+ streaming=${ this . streaming }
84+ > </ shiny-markdown >
13685 ` ;
13786 }
138-
139- updated ( changedProperties : Map < string , unknown > ) : void {
140- if ( changedProperties . has ( "content" ) ) {
141- this . #highlightAndCodeCopy( ) ;
142- if ( this . streaming ) this . #appendStreamingDot( ) ;
143- // It's important that the scroll request happens at this point in time, since
144- // otherwise, the content may not be fully rendered yet
145- requestScroll ( this , this . streaming ) ;
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 ( ) ;
159- }
160-
161- // Highlight code blocks after the element is rendered
162- #highlightAndCodeCopy( ) : void {
163- const el = this . querySelector ( "pre code" ) ;
164- if ( ! el ) return ;
165- this . querySelectorAll < HTMLElement > ( "pre code" ) . forEach ( ( el ) => {
166- // Highlight the code
167- hljs . highlightElement ( el ) ;
168- // Add a button to the code block to copy to clipboard
169- const btn = createElement ( "button" , {
170- class : "code-copy-button" ,
171- title : "Copy to clipboard" ,
172- } ) ;
173- btn . innerHTML = '<i class="bi"></i>' ;
174- el . prepend ( btn ) ;
175- // Add the clipboard functionality
176- const clipboard = new ClipboardJS ( btn , { target : ( ) => el } ) ;
177- clipboard . on ( "success" , function ( e : ClipboardJS . Event ) {
178- btn . classList . add ( "code-copy-button-checked" ) ;
179- setTimeout (
180- ( ) => btn . classList . remove ( "code-copy-button-checked" ) ,
181- 2000
182- ) ;
183- e . clearSelection ( ) ;
184- } ) ;
185- } ) ;
186- }
18787}
18888
18989class ChatUserMessage extends LightElement {
0 commit comments