@@ -9,7 +9,7 @@ import { RegenerateResponseButton } from '../buttons/RegenerateResponseButton';
99import { TTSButton } from '../buttons/TTSButton' ;
1010import FeedbackContentDialog from '../FeedbackContentDialog' ;
1111import { AgentReasoningBubble } from './AgentReasoningBubble' ;
12- import { TickIcon , XIcon } from '../icons' ;
12+ import { DownloadFileIcon , TickIcon , XIcon } from '../icons' ;
1313import { SourceBubble } from '../bubbles/SourceBubble' ;
1414import { DateTimeToggleTheme } from '@/features/bubble/types' ;
1515import { WorkflowTreeView } from '../treeview/WorkflowTreeView' ;
@@ -38,6 +38,8 @@ type Props = {
3838 handleSourceDocumentsClick : ( src : any ) => void ;
3939 onRegenerateResponse ?: ( ) => void ;
4040 onMessageRendered ?: ( ) => void ;
41+ messageRatings ?: Record < string , FeedbackRatingType > ;
42+ onMessageRatingChange ?: ( messageId : string , rating : FeedbackRatingType ) => void ;
4143 // TTS props
4244 isTTSEnabled ?: boolean ;
4345 isTTSLoading ?: Record < string , boolean > ;
@@ -56,93 +58,52 @@ const defaultFeedbackColor = '#3B81F6';
5658export const BotBubble = ( props : Props ) => {
5759 let botDetailsEl : HTMLDetailsElement | undefined ;
5860
59- const DownloadFileIcon = ( ) => (
60- < svg
61- xmlns = "http://www.w3.org/2000/svg"
62- class = "icon icon-tabler icon-tabler-download"
63- width = "24"
64- height = "24"
65- viewBox = "0 0 24 24"
66- stroke-width = "2"
67- stroke = "#ffffff"
68- fill = "none"
69- stroke-linecap = "round"
70- stroke-linejoin = "round"
71- >
72- < path stroke = "none" d = "M0 0h24v24H0z" fill = "none" />
73- < path d = "M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2 -2v-2" />
74- < path d = "M7 11l5 5l5 -5" />
75- < path d = "M12 4l0 12" />
76- </ svg >
77- ) ;
78-
7961 Marked . setOptions ( { isNoP : true , sanitize : props . renderHTML !== undefined ? ! props . renderHTML : true } ) ;
8062
81- const [ rating , setRating ] = createSignal ( '' ) ;
8263 const [ feedbackId , setFeedbackId ] = createSignal ( '' ) ;
8364 const [ showFeedbackContentDialog , setShowFeedbackContentModal ] = createSignal ( false ) ;
8465 const [ copiedMessage , setCopiedMessage ] = createSignal ( false ) ;
85- const [ thumbsUpColor , setThumbsUpColor ] = createSignal ( props . feedbackColor ?? defaultFeedbackColor ) ; // default color
86- const [ thumbsDownColor , setThumbsDownColor ] = createSignal ( props . feedbackColor ?? defaultFeedbackColor ) ; // default color
8766
8867 // Store a reference to the bot message element for the copyMessageToClipboard function
8968 const [ botMessageElement , setBotMessageElement ] = createSignal < HTMLElement | null > ( null ) ;
9069
91- const applyStyles = ( el : HTMLElement ) => {
92- const textColor = props . textColor ?? defaultTextColor ;
93- el . querySelectorAll ( 'a, h1, h2, h3, h4, h5, h6, strong, em, blockquote, li' ) . forEach ( ( element ) => {
94- ( element as HTMLElement ) . style . color = textColor ;
95- } ) ;
96- el . querySelectorAll ( 'pre' ) . forEach ( ( element ) => {
97- ( element as HTMLElement ) . style . color = '#FFFFFF' ;
98- element . querySelectorAll ( 'code' ) . forEach ( ( codeElement ) => {
99- ( codeElement as HTMLElement ) . style . color = '#FFFFFF' ;
100- } ) ;
101- } ) ;
102- el . querySelectorAll ( 'code:not(pre code)' ) . forEach ( ( element ) => {
103- ( element as HTMLElement ) . style . color = '#4CAF50' ;
104- } ) ;
105- el . querySelectorAll ( 'a' ) . forEach ( ( link ) => {
106- link . target = '_blank' ;
107- } ) ;
70+ const currentRating = ( ) => {
71+ const messageId = props . message . messageId ;
72+ if ( ! messageId ) return props . message . rating ?? '' ;
73+ return props . messageRatings ?. [ messageId ] ?? props . message . rating ?? '' ;
74+ } ;
75+
76+ const thumbsUpColor = ( ) => ( currentRating ( ) === 'THUMBS_UP' ? '#006400' : props . feedbackColor ?? defaultFeedbackColor ) ;
77+ const thumbsDownColor = ( ) => ( currentRating ( ) === 'THUMBS_DOWN' ? '#8B0000' : props . feedbackColor ?? defaultFeedbackColor ) ;
78+
79+ const renderMarkdownHtml = ( content : string ) => {
80+ const html = Marked . parse ( content ) ;
81+ return html . replace ( / < a (? ! [ ^ > ] * \b t a r g e t = ) ( [ ^ > ] * ) > / g, '<a target="_blank" rel="noopener noreferrer"$1>' ) ;
10882 } ;
10983
11084 const setBotMessageRef = ( el : HTMLSpanElement | null ) => {
11185 setBotMessageElement ( el ) ;
11286 } ;
11387
88+ const notifyMessageRendered = ( ) => {
89+ props . onMessageRendered ?.( ) ;
90+ } ;
91+
11492 createEffect ( ( ) => {
11593 const el = botMessageElement ( ) ;
11694 const message = props . message . message ?? '' ;
11795 if ( ! el ) return ;
11896
11997 // Update innerHTML synchronously so the DOM reflects the correct height
12098 // before any scroll logic runs — avoids async mismatch that causes jumping.
121- el . innerHTML = Marked . parse ( message ) ;
122- applyStyles ( el ) ;
123-
124- // Only wire image callbacks and notify after streaming ends.
125- if ( ! props . isLoading ) {
126- el . querySelectorAll ( 'img' ) . forEach ( ( img ) => {
127- if ( ( img as HTMLImageElement ) . complete ) return ;
128- img . addEventListener ( 'load' , ( ) => props . onMessageRendered ?.( ) , { once : true } ) ;
129- img . addEventListener ( 'error' , ( ) => props . onMessageRendered ?.( ) , { once : true } ) ;
130- } ) ;
131- props . onMessageRendered ?.( ) ;
132- }
133- } ) ;
99+ el . innerHTML = renderMarkdownHtml ( message ) ;
134100
135- createEffect ( ( ) => {
136- const nextRating = props . message . rating ;
137- if ( ! nextRating ) return ;
138- setRating ( nextRating ) ;
139- if ( nextRating === 'THUMBS_UP' ) {
140- setThumbsUpColor ( '#006400' ) ;
141- return ;
142- }
143- if ( nextRating === 'THUMBS_DOWN' ) {
144- setThumbsDownColor ( '#8B0000' ) ;
145- }
101+ el . querySelectorAll ( 'img' ) . forEach ( ( img ) => {
102+ if ( ( img as HTMLImageElement ) . complete ) return ;
103+ img . addEventListener ( 'load' , notifyMessageRendered , { once : true } ) ;
104+ img . addEventListener ( 'error' , notifyMessageRendered , { once : true } ) ;
105+ } ) ;
106+ notifyMessageRendered ( ) ;
146107 } ) ;
147108
148109 createEffect ( ( ) => {
@@ -221,11 +182,13 @@ export const BotBubble = (props: Props) => {
221182 } ;
222183
223184 const onThumbsUpClick = async ( ) => {
224- if ( rating ( ) === '' ) {
185+ if ( currentRating ( ) === '' ) {
186+ const messageId = props . message ?. messageId ;
187+ if ( ! messageId ) return ;
225188 const body = {
226189 chatflowid : props . chatflowid ,
227190 chatId : props . chatId ,
228- messageId : props . message ?. messageId as string ,
191+ messageId,
229192 rating : 'THUMBS_UP' as FeedbackRatingType ,
230193 content : '' ,
231194 } ;
@@ -240,22 +203,22 @@ export const BotBubble = (props: Props) => {
240203 const data = result . data as any ;
241204 let id = '' ;
242205 if ( data && data . id ) id = data . id ;
243- setRating ( 'THUMBS_UP' ) ;
206+ props . onMessageRatingChange ?. ( messageId , 'THUMBS_UP' ) ;
244207 setFeedbackId ( id ) ;
245208 setShowFeedbackContentModal ( true ) ;
246- // update the thumbs up color state
247- setThumbsUpColor ( '#006400' ) ;
248209 saveToLocalStorage ( 'THUMBS_UP' ) ;
249210 }
250211 }
251212 } ;
252213
253214 const onThumbsDownClick = async ( ) => {
254- if ( rating ( ) === '' ) {
215+ if ( currentRating ( ) === '' ) {
216+ const messageId = props . message ?. messageId ;
217+ if ( ! messageId ) return ;
255218 const body = {
256219 chatflowid : props . chatflowid ,
257220 chatId : props . chatId ,
258- messageId : props . message ?. messageId as string ,
221+ messageId,
259222 rating : 'THUMBS_DOWN' as FeedbackRatingType ,
260223 content : '' ,
261224 } ;
@@ -270,11 +233,9 @@ export const BotBubble = (props: Props) => {
270233 const data = result . data as any ;
271234 let id = '' ;
272235 if ( data && data . id ) id = data . id ;
273- setRating ( 'THUMBS_DOWN' ) ;
236+ props . onMessageRatingChange ?. ( messageId , 'THUMBS_DOWN' ) ;
274237 setFeedbackId ( id ) ;
275238 setShowFeedbackContentModal ( true ) ;
276- // update the thumbs down color state
277- setThumbsDownColor ( '#8B0000' ) ;
278239 saveToLocalStorage ( 'THUMBS_DOWN' ) ;
279240 }
280241 }
@@ -312,11 +273,6 @@ export const BotBubble = (props: Props) => {
312273 } ) ;
313274
314275 const renderArtifacts = ( item : Partial < FileUpload > ) => {
315- // Instead of onMount, we'll use a callback ref to apply styles
316- const setArtifactRef = ( el : HTMLSpanElement ) => {
317- if ( el ) applyStyles ( el ) ;
318- } ;
319-
320276 return (
321277 < >
322278 < Show when = { item . type === 'png' || item . type === 'jpeg' } >
@@ -344,14 +300,16 @@ export const BotBubble = (props: Props) => {
344300 </ Show >
345301 < Show when = { item . type !== 'png' && item . type !== 'jpeg' && item . type !== 'html' } >
346302 < span
347- ref = { setArtifactRef }
348- innerHTML = { Marked . parse ( item . data as string ) }
349- class = "prose"
303+ innerHTML = { renderMarkdownHtml ( item . data as string ) }
304+ class = "prose bot-markdown-content"
350305 style = { {
351306 'background-color' : props . backgroundColor ?? defaultBackgroundColor ,
352307 color : props . textColor ?? defaultTextColor ,
353308 'border-radius' : '6px' ,
354309 'font-size' : props . fontSize ? `${ props . fontSize } px` : `${ defaultFontSize } px` ,
310+ '--bot-markdown-text-color' : props . textColor ?? defaultTextColor ,
311+ '--bot-markdown-code-color' : '#FFFFFF' ,
312+ '--bot-markdown-inline-code-color' : '#4CAF50' ,
355313 } }
356314 />
357315 </ Show >
@@ -478,13 +436,16 @@ export const BotBubble = (props: Props) => {
478436 < >
479437 < span
480438 ref = { setBotMessageRef }
481- class = "px-4 py-2 ml-2 max-w-full chatbot-host-bubble prose"
439+ class = "px-4 py-2 ml-2 max-w-full chatbot-host-bubble prose bot-markdown-content "
482440 data-testid = "host-bubble"
483441 style = { {
484442 'background-color' : props . backgroundColor ?? defaultBackgroundColor ,
485443 color : props . textColor ?? defaultTextColor ,
486444 'border-radius' : '6px' ,
487445 'font-size' : props . fontSize ? `${ props . fontSize } px` : `${ defaultFontSize } px` ,
446+ '--bot-markdown-text-color' : props . textColor ?? defaultTextColor ,
447+ '--bot-markdown-code-color' : '#FFFFFF' ,
448+ '--bot-markdown-inline-code-color' : '#4CAF50' ,
488449 } }
489450 />
490451 < For each = { props . fileAnnotations || [ ] } >
@@ -611,14 +572,19 @@ export const BotBubble = (props: Props) => {
611572 Copied!
612573 </ div >
613574 </ Show >
614- { rating ( ) === '' || rating ( ) === 'THUMBS_UP' ? (
615- < ThumbsUpButton feedbackColor = { thumbsUpColor ( ) } isDisabled = { rating ( ) === 'THUMBS_UP' } rating = { rating ( ) } onClick = { onThumbsUpClick } />
575+ { currentRating ( ) === '' || currentRating ( ) === 'THUMBS_UP' ? (
576+ < ThumbsUpButton
577+ feedbackColor = { thumbsUpColor ( ) }
578+ isDisabled = { currentRating ( ) === 'THUMBS_UP' }
579+ rating = { currentRating ( ) }
580+ onClick = { onThumbsUpClick }
581+ />
616582 ) : null }
617- { rating ( ) === '' || rating ( ) === 'THUMBS_DOWN' ? (
583+ { currentRating ( ) === '' || currentRating ( ) === 'THUMBS_DOWN' ? (
618584 < ThumbsDownButton
619585 feedbackColor = { thumbsDownColor ( ) }
620- isDisabled = { rating ( ) === 'THUMBS_DOWN' }
621- rating = { rating ( ) }
586+ isDisabled = { currentRating ( ) === 'THUMBS_DOWN' }
587+ rating = { currentRating ( ) }
622588 onClick = { onThumbsDownClick }
623589 />
624590 ) : null }
0 commit comments