@@ -40,7 +40,6 @@ const defaultFontSize = 16;
4040const defaultFeedbackColor = '#3B81F6' ;
4141
4242export const BotBubble = ( props : Props ) => {
43- let botMessageEl : HTMLDivElement | undefined ;
4443 let botDetailsEl : HTMLDetailsElement | undefined ;
4544
4645 Marked . setOptions ( { isNoP : true , sanitize : props . renderHTML !== undefined ? ! props . renderHTML : true } ) ;
@@ -51,6 +50,55 @@ export const BotBubble = (props: Props) => {
5150 const [ copiedMessage , setCopiedMessage ] = createSignal ( false ) ;
5251 const [ thumbsUpColor , setThumbsUpColor ] = createSignal ( props . feedbackColor ?? defaultFeedbackColor ) ; // default color
5352 const [ thumbsDownColor , setThumbsDownColor ] = createSignal ( props . feedbackColor ?? defaultFeedbackColor ) ; // default color
53+
54+ // Store a reference to the bot message element for the copyMessageToClipboard function
55+ const [ botMessageElement , setBotMessageElement ] = createSignal < HTMLElement | null > ( null ) ;
56+
57+ const setBotMessageRef = ( el : HTMLSpanElement ) => {
58+ if ( el ) {
59+ el . innerHTML = Marked . parse ( props . message . message ) ;
60+
61+ // Apply textColor to all links, headings, and other markdown elements
62+ const textColor = props . textColor ?? defaultTextColor ;
63+ el . querySelectorAll ( 'a, h1, h2, h3, h4, h5, h6, strong, em, blockquote, li, code, pre' ) . forEach ( ( element ) => {
64+ ( element as HTMLElement ) . style . color = textColor ;
65+ } ) ;
66+
67+ // Set target="_blank" for links
68+ el . querySelectorAll ( 'a' ) . forEach ( ( link ) => {
69+ link . target = '_blank' ;
70+ } ) ;
71+
72+ // Store the element ref for the copy function
73+ setBotMessageElement ( el ) ;
74+
75+ if ( props . message . rating ) {
76+ setRating ( props . message . rating ) ;
77+ if ( props . message . rating === 'THUMBS_UP' ) {
78+ setThumbsUpColor ( '#006400' ) ;
79+ } else if ( props . message . rating === 'THUMBS_DOWN' ) {
80+ setThumbsDownColor ( '#8B0000' ) ;
81+ }
82+ }
83+ if ( props . fileAnnotations && props . fileAnnotations . length ) {
84+ for ( const annotations of props . fileAnnotations ) {
85+ const button = document . createElement ( 'button' ) ;
86+ button . textContent = annotations . fileName ;
87+ button . className =
88+ 'py-2 px-4 mb-2 justify-center font-semibold text-white focus:outline-none flex items-center disabled:opacity-50 disabled:cursor-not-allowed disabled:brightness-100 transition-all filter hover:brightness-90 active:brightness-75 file-annotation-button' ;
89+ button . addEventListener ( 'click' , function ( ) {
90+ downloadFile ( annotations ) ;
91+ } ) ;
92+ const svgContainer = document . createElement ( 'div' ) ;
93+ svgContainer . className = 'ml-2' ;
94+ svgContainer . innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-download" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="#ffffff" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2 -2v-2" /><path d="M7 11l5 5l5 -5" /><path d="M12 4l0 12" /></svg>` ;
95+
96+ button . appendChild ( svgContainer ) ;
97+ el . appendChild ( button ) ;
98+ }
99+ }
100+ }
101+ } ;
54102
55103 const downloadFile = async ( fileAnnotation : any ) => {
56104 try {
@@ -74,7 +122,7 @@ export const BotBubble = (props: Props) => {
74122
75123 const copyMessageToClipboard = async ( ) => {
76124 try {
77- const text = botMessageEl ? botMessageEl ?. textContent : '' ;
125+ const text = botMessageElement ( ) ? botMessageElement ( ) ?. textContent : '' ;
78126 await navigator . clipboard . writeText ( text || '' ) ;
79127 setCopiedMessage ( true ) ;
80128 setTimeout ( ( ) => {
@@ -201,38 +249,6 @@ export const BotBubble = (props: Props) => {
201249 } ;
202250
203251 onMount ( ( ) => {
204- if ( botMessageEl ) {
205- botMessageEl . innerHTML = Marked . parse ( props . message . message ) ;
206- botMessageEl . querySelectorAll ( 'a' ) . forEach ( ( link ) => {
207- link . target = '_blank' ;
208- } ) ;
209- if ( props . message . rating ) {
210- setRating ( props . message . rating ) ;
211- if ( props . message . rating === 'THUMBS_UP' ) {
212- setThumbsUpColor ( '#006400' ) ;
213- } else if ( props . message . rating === 'THUMBS_DOWN' ) {
214- setThumbsDownColor ( '#8B0000' ) ;
215- }
216- }
217- if ( props . fileAnnotations && props . fileAnnotations . length ) {
218- for ( const annotations of props . fileAnnotations ) {
219- const button = document . createElement ( 'button' ) ;
220- button . textContent = annotations . fileName ;
221- button . className =
222- 'py-2 px-4 mb-2 justify-center font-semibold text-white focus:outline-none flex items-center disabled:opacity-50 disabled:cursor-not-allowed disabled:brightness-100 transition-all filter hover:brightness-90 active:brightness-75 file-annotation-button' ;
223- button . addEventListener ( 'click' , function ( ) {
224- downloadFile ( annotations ) ;
225- } ) ;
226- const svgContainer = document . createElement ( 'div' ) ;
227- svgContainer . className = 'ml-2' ;
228- svgContainer . innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-download" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="#ffffff" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2 -2v-2" /><path d="M7 11l5 5l5 -5" /><path d="M12 4l0 12" /></svg>` ;
229-
230- button . appendChild ( svgContainer ) ;
231- botMessageEl . appendChild ( button ) ;
232- }
233- }
234- }
235-
236252 if ( botDetailsEl && props . isLoading ) {
237253 botDetailsEl . open = true ;
238254 }
@@ -247,6 +263,20 @@ export const BotBubble = (props: Props) => {
247263 } ) ;
248264
249265 const renderArtifacts = ( item : Partial < FileUpload > ) => {
266+ // Instead of onMount, we'll use a callback ref to apply styles
267+ const setArtifactRef = ( el : HTMLSpanElement ) => {
268+ if ( el ) {
269+ const textColor = props . textColor ?? defaultTextColor ;
270+ el . querySelectorAll ( 'a, h1, h2, h3, h4, h5, h6, strong, em, blockquote, li, code, pre' ) . forEach ( ( element ) => {
271+ ( element as HTMLElement ) . style . color = textColor ;
272+ } ) ;
273+
274+ el . querySelectorAll ( 'a' ) . forEach ( ( link ) => {
275+ link . target = '_blank' ;
276+ } ) ;
277+ }
278+ } ;
279+
250280 return (
251281 < >
252282 < Show when = { item . type === 'png' || item . type === 'jpeg' } >
@@ -271,6 +301,7 @@ export const BotBubble = (props: Props) => {
271301 </ Show >
272302 < Show when = { item . type !== 'png' && item . type !== 'jpeg' && item . type !== 'html' } >
273303 < span
304+ ref = { setArtifactRef }
274305 innerHTML = { Marked . parse ( item . data as string ) }
275306 class = "prose"
276307 style = { {
@@ -383,7 +414,7 @@ export const BotBubble = (props: Props) => {
383414 ) }
384415 { props . message . message && (
385416 < span
386- ref = { botMessageEl }
417+ ref = { setBotMessageRef }
387418 class = "px-4 py-2 ml-2 max-w-full chatbot-host-bubble prose"
388419 data-testid = "host-bubble"
389420 style = { {
0 commit comments