@@ -423,6 +423,102 @@ const convertLatexDelimiters = (content: string): string => {
423423 ) ;
424424} ;
425425
426+ // Video component with error handling - defined outside to prevent re-creation on each render
427+ interface VideoWithErrorHandlingProps {
428+ src : string ;
429+ alt ?: string | null ;
430+ props ?: React . VideoHTMLAttributes < HTMLVideoElement > ;
431+ }
432+
433+ const VideoWithErrorHandling : React . FC < VideoWithErrorHandlingProps > = React . memo ( ( { src, alt, props = { } } ) => {
434+ const { t } = useTranslation ( "common" ) ;
435+ const [ hasError , setHasError ] = React . useState ( false ) ;
436+
437+ if ( hasError ) {
438+ return (
439+ < div className = "markdown-media-error" >
440+ < div className = "markdown-media-error-message" >
441+ { t ( "chatStreamMessage.videoLinkUnavailable" , {
442+ defaultValue : "This video link is unavailable" ,
443+ } ) }
444+ </ div >
445+ { alt && (
446+ < div className = "markdown-media-error-caption" > { alt } </ div >
447+ ) }
448+ </ div >
449+ ) ;
450+ }
451+
452+ return (
453+ < figure className = "markdown-video-wrapper" >
454+ < video
455+ className = "markdown-video"
456+ controls
457+ preload = "metadata"
458+ playsInline
459+ src = { src }
460+ onError = { ( ) => setHasError ( true ) }
461+ { ...props }
462+ >
463+ { t ( "chatStreamMessage.videoNotSupported" , {
464+ defaultValue : "Sorry, your browser does not support embedded videos." ,
465+ } ) }
466+ </ video >
467+ { alt ? (
468+ < figcaption className = "markdown-video-caption" > { alt } </ figcaption >
469+ ) : null }
470+ </ figure >
471+ ) ;
472+ } , ( prevProps , nextProps ) => {
473+ // Custom comparison function to prevent unnecessary re-renders
474+ // Only compare src and alt, props object reference may change but content is the same
475+ return prevProps . src === nextProps . src &&
476+ prevProps . alt === nextProps . alt ;
477+ } ) ;
478+
479+ VideoWithErrorHandling . displayName = "VideoWithErrorHandling" ;
480+
481+ // Image component with error handling - defined outside to prevent re-creation on each render
482+ interface ImageWithErrorHandlingProps {
483+ src : string ;
484+ alt ?: string | null ;
485+ }
486+
487+ const ImageWithErrorHandling : React . FC < ImageWithErrorHandlingProps > = React . memo ( ( { src, alt } ) => {
488+ const { t } = useTranslation ( "common" ) ;
489+ const [ hasError , setHasError ] = React . useState ( false ) ;
490+
491+ if ( hasError ) {
492+ return (
493+ < div className = "markdown-media-error" >
494+ < div className = "markdown-media-error-message" >
495+ { t ( "chatStreamMessage.imageLinkUnavailable" , {
496+ defaultValue : "This image link is unavailable" ,
497+ } ) }
498+ </ div >
499+ { alt && (
500+ < div className = "markdown-media-error-caption" > { alt } </ div >
501+ ) }
502+ </ div >
503+ ) ;
504+ }
505+
506+ return (
507+ < img
508+ src = { src }
509+ alt = { alt ?? undefined }
510+ className = "markdown-img"
511+ onError = { ( ) => setHasError ( true ) }
512+ />
513+ ) ;
514+ } , ( prevProps , nextProps ) => {
515+ // Custom comparison function to prevent unnecessary re-renders
516+ return prevProps . src === nextProps . src &&
517+ prevProps . alt === nextProps . alt ;
518+ } ) ;
519+
520+ ImageWithErrorHandling . displayName = "ImageWithErrorHandling" ;
521+
426522export const MarkdownRenderer : React . FC < MarkdownRendererProps > = ( {
427523 content,
428524 className,
@@ -517,47 +613,7 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
517613 return renderMediaFallback ( src , alt ) ;
518614 }
519615
520- const VideoWithErrorHandling = ( ) => {
521- const [ hasError , setHasError ] = React . useState ( false ) ;
522-
523- if ( hasError ) {
524- return (
525- < div className = "markdown-media-error" >
526- < div className = "markdown-media-error-message" >
527- { t ( "chatStreamMessage.videoLinkUnavailable" , {
528- defaultValue : "This video link is unavailable" ,
529- } ) }
530- </ div >
531- { alt && (
532- < div className = "markdown-media-error-caption" > { alt } </ div >
533- ) }
534- </ div >
535- ) ;
536- }
537-
538- return (
539- < figure className = "markdown-video-wrapper" >
540- < video
541- className = "markdown-video"
542- controls
543- preload = "metadata"
544- playsInline
545- src = { src }
546- onError = { ( ) => setHasError ( true ) }
547- { ...props }
548- >
549- { t ( "chatStreamMessage.videoNotSupported" , {
550- defaultValue : "Sorry, your browser does not support embedded videos." ,
551- } ) }
552- </ video >
553- { alt ? (
554- < figcaption className = "markdown-video-caption" > { alt } </ figcaption >
555- ) : null }
556- </ figure >
557- ) ;
558- } ;
559-
560- return < VideoWithErrorHandling /> ;
616+ return < VideoWithErrorHandling key = { src } src = { src } alt = { alt } props = { props } /> ;
561617 } ;
562618
563619 // Modified processText function logic
@@ -861,35 +917,11 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
861917 return renderVideoElement ( { src, alt } ) ;
862918 }
863919
864- const ImageWithErrorHandling = ( ) => {
865- const [ hasError , setHasError ] = React . useState ( false ) ;
866-
867- if ( hasError ) {
868- return (
869- < div className = "markdown-media-error" >
870- < div className = "markdown-media-error-message" >
871- { t ( "chatStreamMessage.imageLinkUnavailable" , {
872- defaultValue : "This image link is unavailable" ,
873- } ) }
874- </ div >
875- { alt && (
876- < div className = "markdown-media-error-caption" > { alt } </ div >
877- ) }
878- </ div >
879- ) ;
880- }
920+ if ( ! src || typeof src !== "string" ) {
921+ return null ;
922+ }
881923
882- return (
883- < img
884- src = { src }
885- alt = { alt }
886- className = "markdown-img"
887- onError = { ( ) => setHasError ( true ) }
888- />
889- ) ;
890- } ;
891-
892- return < ImageWithErrorHandling /> ;
924+ return < ImageWithErrorHandling key = { src } src = { src } alt = { alt } /> ;
893925 } ,
894926 // Video
895927 video : ( { children, ...props } : any ) => {
0 commit comments